Topic 14.6
Proxy and Reflect
In one line
A Proxy wraps an object and lets you intercept basic operations on it (reading, writing, deleting, in, calling) with handler functions called traps; Reflect offers the default version of each operation, so a trap can add behaviour and then do the normal thing.
Think of it like this
A receptionist in front of a manager's office. Visitors don't walk straight in: they talk to the receptionist, who can log the visit, check an ID, turn someone away, or pass the message on unchanged. A Proxy is that receptionist for an object. The manager is the target object, and the receptionist's rule book is the handler.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Proxy
- An object that stands in front of another object and can intercept what you do to it.
- Target
- The real object a proxy wraps.
- Handler
- The object holding the trap functions for a proxy.
- Trap
- A handler function that runs instead of a basic operation, like
getfor reading a property. - Reflect
- A built-in object with functions that do each basic object operation the normal way.
- Receiver
- The object an operation started on (usually the proxy), passed to
getandsettraps so getters and setters see the rightthis. - Invariant
- A rule a trap must not break, such as reporting a frozen property's real value.
- Reactivity
- Updating the screen automatically when data changes, by noticing reads and writes.
Step by step
01Wrap an object with an empty handler
With no traps, every operation passes straight through. The proxy looks and acts like the target, but it's a separate object.
const target = { name: "Asha" };
const p = new Proxy(target, {});
p.age = 12;
console.log(target.age); // 12: the write reached the target
console.log(p === target); // false02Add a get trap
A trap replaces the operation. Use Reflect inside it to carry out the default and add your own behaviour around it.
03Validate writes in a set trap
Check the value. If it's bad, throw a helpful error. If it's fine, store it with Reflect.set and return its result (true).
const person = new Proxy({}, {
set(target, key, value, receiver) {
if (key === "age" && !Number.isInteger(value)) {
throw new TypeError("age must be an integer");
}
return Reflect.set(target, key, value, receiver);
},
});
person.age = 30; // fine
person.age = "old"; // TypeError: age must be an integer04Trap function calls
For a function target, apply runs on every call. Here it measures how often a function is called without changing it.
let calls = 0;
const counted = new Proxy(Math.max, {
apply(target, thisArg, args) {
calls++;
return Reflect.apply(target, thisArg, args);
},
});
counted(1, 5);
counted(2, 3);
console.log(calls); // 205Make it reactive
Call a function whenever a property changes. Frameworks extend this by recording, in the get trap, which render functions read which properties, so only those re-run.
06Revoke access
Proxy.revocable(target, handler) returns { proxy, revoke }. After revoke(), any operation on the proxy throws a TypeError. Handy for handing out temporary access to an object.
Try it yourself
- 1
Make it read-only
In the first example, change the
settrap to logblockedandreturn truewithout callingReflect.set. Predict whattargetlooks like at the end. - 2
Validate a name
In the second example, add a
namerule: a string of at least 2 characters. Predict which ofuser.name = "A"anduser.name = "Asha"throws. - 3
Add a second display
In the reactive example, add a
<p>Squared: <strong id="square"></strong></p>and update it inrender. Click +1 and watch all three update from one write.
Code & diagrams
Every change went through to `target`, but you saw it happen. Logging the proxy itself would fire `get` many times, so we log the target.
Preview
Console
Nothing logged yet.Expected console output
get name
set age = 13
has age
true
delete age
Asha { name: 'Asha' }
falseProperty keys always reach traps as strings (or symbols), so `list[-1]` arrives as the string `"-1"`. Checking the type first matters: `Number(symbol)` would throw when the array is spread.
Preview
Console
Nothing logged yet.Expected console output
Invalid value for age: -5
30
3 0
30 10 10 3No code calls `render()` after changing the count: the proxy notices writes. Setting the same value twice doesn't re-render. Click +1 to see more lines.
Preview
Console
Nothing logged yet.Expected console output
count: 0 -> 5
count: 5 -> 6Break it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Return false from a set trap in strict code
In a module or after "use strict", use set() { return false; } and then write p.age = 1.
Break #2
Read through the proxy inside its own get trap
Write get(t, key, receiver) { return receiver[key]; }.
Break #3
Call a method that uses #private fields
Wrap an instance of class C { #x = 1; getX() { return this.#x; } } in new Proxy(obj, {}) and call proxy.getX().
Myth vs fact
Myth
A Proxy copies the target.
Fact
It wraps it. Writes that pass through change the target, and changes to the target are visible through the proxy.
Myth
Proxies are too slow to use anywhere.
Fact
Each trapped operation costs more than a plain property access, and engines can't optimise proxies as well. That matters in hot loops, not for state objects that change on clicks. Vue 3 uses proxies for all reactive state.
Myth
Reflect is a different way to do reflection like in Java.
Fact
It's simply the set of default object operations, as functions, matching the 13 traps one to one.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
Proxies trap only the 13 internal methods. They can't intercept
===(identity),typeof, or access to internal slots:new Proxy(new Map(), {}).get("a")throwsTypeError: Method Map.prototype.get called on incompatible receiver #<Map>, because Map methods need the real Map's internal data. Bind such methods to the target in thegettrap. - ▸
Always pass
receivertoReflect.get/Reflect.set. If the target has a getter or setter,receiverdecides whatthisis inside it. Dropping it breaks getters that read other properties, and breaks proxies used as prototypes. - ▸
Proxy.revocablelets you build membranes: wrap an object graph so every object handed out is also proxied, then cut all access with onerevoke(). Secure sandboxes (and Agoric's SES) use this pattern. - ▸
Proxies are not observable as such: there's no
isProxy()in the language,Array.isArray(new Proxy([], {}))istrue, andtypeofon a function proxy is"function". DevTools does showProxy(Object)and the handler.
Remember this
- 1
new Proxy(target, handler)creates a proxy: a new object that stands in fortarget. Every basic operation on the proxy first looks for a matching function inhandler, called a trap. If there's a trap, it runs; if not, the operation goes straight to the target. An empty handler{}gives a proxy that behaves exactly like the target. - 2
There are 13 traps, one for each internal operation the spec defines on objects:
get(readingp.x),set(writingp.x = 1),has("x" in p),deleteProperty(delete p.x),ownKeys(Object.keys,for...in,Reflect.ownKeys),getOwnPropertyDescriptor,defineProperty,getPrototypeOf,setPrototypeOf,isExtensible,preventExtensions, and for function targetsapply(callingp()) andconstruct(new p()). Each trap receives the target, the property key and, forget/set, the receiver (the object the operation started on, usually the proxy). - 3
Reflect is a built-in object with one function for each trap, with the same name and arguments:
Reflect.get(target, key, receiver),Reflect.set(...),Reflect.has(...),Reflect.ownKeys(...)and so on. Each does the default behaviour. So the usual pattern is: do your extra work, thenreturn Reflect.set(target, key, value, receiver). Reflect methods also returntrue/falseinstead of throwing where the oldObjectmethods threw (Reflect.definePropertyvsObject.defineProperty), andReflect.ownKeyslists string and symbol keys. - 4
A
settrap must returntruewhen the write succeeded. If it returnsfalse, sloppy-mode code ignores the failure silently, but strict-mode code (modules, classes,"use strict", see Topic 14.9) throwsTypeError: 'set' on proxy: trap returned falsish for property 'age'. For validation, either throw your own clear error from the trap or returnfalseand rely on strict mode. - 5
Traps must respect invariants: rules that keep the language consistent. For example, if the target has a non-configurable, non-writable property, the
gettrap must report its real value;ownKeysmust include every non-configurable key. Breaking an invariant throws aTypeError. A proxy is also a different object from its target (proxy !== target), and methods that use#privatefields fail when called on a proxy, because the proxy doesn't have the target's private slots. - 6
Uses: validation (reject bad values as they're written), defaults (return
0for missing keys), logging and debugging, negative array indexes, read-only views, revocable access (Proxy.revocablegives a proxy you can switch off), and reactivity. Vue 3's reactivity system wraps your state in proxies: thegettrap records which component read which property, and thesettrap re-renders exactly those components. That's the idea you'll build in the third example.
Explain it without notes
What is a Proxy and what is a trap? Give three traps and the operations they intercept.
Why is Reflect usually used inside traps?
How would you validate property values with a Proxy, and how does strict mode affect it?
Explain how a Proxy can make UI reactive, and what its limits are.
Practice
Write readOnly(obj) that returns a proxy where reads work but any write or delete throws Error("read-only"). Show both behaviours.
Write a proxy env where reading a missing key throws ReferenceError("Unknown setting: <key>") instead of returning undefined, so typos are caught.
Trade-offs
- ↔
Proxies add powerful behaviour without changing the code that uses the object, but that "magic" makes code harder to read and debug, and every trapped operation is slower than a plain one.
- ↔
Validation in a proxy catches every write in one place; explicit setter methods or a schema check at the boundary are more visible and work with TypeScript types more naturally.
Done when you can
I can create a Proxy with
get,set,hasanddeletePropertytraps.I can use
Reflectto carry out the default behaviour, passing the receiver.I can write a validating proxy and know how strict mode treats
falsefromset.I can explain the reactivity idea behind Vue-style frameworks.
I know proxy limits: identity, private fields, internal slots and invariants.