JavaScript · Modules, workers & advanced
Proxy & Reflect
⚡ TL;DR — A
Proxywraps an object and lets you intercept the fundamental operations on it — get, set, has, delete, call — via "traps."Reflectgives you the default behaviour of each of those operations as a plain function, so inside a trap you do the normal thing and then add your logic around it.
🧠 Mental model
Every object operation — reading a property, assigning one, checking in, calling as a function — is a low-level internal method the engine invokes ([[Get]], [[Set]], [[Has]]…). Normally these are hard-wired. A Proxy lets you substitute your own implementation for any of them.
code: proxy.name
↓ engine calls [[Get]]
get trap runs → you decide what "reading .name" means
Think of a Proxy as a customs checkpoint in front of an object: nothing gets in or out without passing through your handler, and you can inspect, modify, block, or log every operation. Reflect is the checkpoint's "just do the normal thing" button — Reflect.get(target, key, receiver) performs the default read. The two are designed as a matched pair: every trap has a same-named, same-signature Reflect method.
This is the machinery behind Vue 3's reactivity, MobX, Immer, and validation/ORM libraries.
⚙️ How it actually works
A proxy is new Proxy(target, handler). The handler's methods are traps — omit a trap and that operation passes through to the target untouched. The common ones:
| Trap | Fires on | Reflect counterpart |
|---|---|---|
get |
obj.x, obj['x'] |
Reflect.get |
set |
obj.x = v |
Reflect.set |
has |
'x' in obj |
Reflect.has |
deleteProperty |
delete obj.x |
Reflect.deleteProperty |
apply |
fn(...) |
Reflect.apply |
construct |
new Fn(...) |
Reflect.construct |
ownKeys |
Object.keys, spread |
Reflect.ownKeys |
Two staff-level reasons Reflect is not optional:
- The
receiverand getters/inheritance.return target[key]breaks when a getter on the prototype referencesthis—thiswould be the raw target, not the proxy, so nested reactive reads escape tracking.Reflect.get(target, key, receiver)forwards the correctreceiver, sothisstays the proxy. This is the reason Vue switched to Proxy+Reflect. - Invariants. Traps must obey consistency rules — e.g. you can't report a non-configurable, non-writable property as a different value, or hide a non-configurable own property from
ownKeys. Violate an invariant and the engine throws aTypeError. Delegating throughReflectkeeps you honest automatically.
Proxy.revocable(target, handler) returns { proxy, revoke }; calling revoke() makes every future operation throw — a clean way to invalidate a capability (e.g. tear down access after a component unmounts).
💻 Code
// Reactive object: track reads, react to writes — the core of Vue-style reactivity.
function reactive(target, onChange) {
return new Proxy(target, {
get(t, key, receiver) {
const value = Reflect.get(t, key, receiver); // correct `this` for getters
return typeof value === 'object' && value !== null
? reactive(value, onChange) // deep: proxy nested objects lazily
: value;
},
set(t, key, value, receiver) {
const ok = Reflect.set(t, key, value, receiver); // do the real write
onChange(key, value); // then react
return ok; // MUST return boolean
},
});
}
// Validation: reject bad writes at assignment time.
const user = new Proxy({}, {
set(t, key, value) {
if (key === 'age' && !Number.isInteger(value)) throw new TypeError('age must be int');
return Reflect.set(t, key, value);
},
});
user.age = 30; // ok
user.age = 'x'; // throws
// ❌ Naive trap: breaks inherited getters and forgets the return value.
new Proxy(obj, { get: (t, k) => t[k], set: (t, k, v) => { t[k] = v; } }); // set returns undefined → strict-mode TypeError
⚖️ Trade-offs
- Use for cross-cutting interception you can't get otherwise: reactivity/observation, validation, negative-array-index or default-value objects, API mocking, access control, lazy-loading/hydration. When the behaviour of the object itself must change, Proxy is the only clean tool.
- When NOT to use it: anything hot. Every trapped operation is a function call through the handler — a proxied object property read is meaningfully slower than a plain one, and it defeats some JIT optimisations. Never proxy a hot inner-loop data structure.
- Not fully transparent.
proxy === targetisfalse;WeakMap/Mapkeyed by the target won't find the proxy; some engine internals and private class fields (#x) don't play nicely. It looks like the object but isn't identical to it. - Debuggability. Traps make "why did reading this property do that?" genuinely hard to trace. Keep handlers small and obvious.
💣 Gotchas interviewers probe
- Why
Reflectat all? The answer isreceiver/thiscorrectness for getters and prototype chains, plus invariant preservation. "It's just a nicer syntax" is a shallow answer — it's about correct forwarding. setanddeletePropertymust return a boolean. Returningundefined(falsy) signals failure and throws in strict mode. Silent data loss otherwise. Extremely common bug.- Traps don't fire recursively. Proxying an object does not proxy its nested objects — you must wrap them on access (see
getabove) for deep reactivity. Candidates assume it's deep. - Private fields escape.
#fieldaccess inside a class method uses the realthis, not the proxy, so proxies can't intercept private fields — a known limitation. - Proxy breaks identity.
proxy !== target, and it won't be found as aWeakMap/Setkey registered under the raw target. Reactivity libs keep atarget → proxymap to dedupe. - Performance. If asked "why not proxy everything?", cite the per-operation trap overhead and lost JIT optimisation.
🎯 Say this in the interview
"A Proxy intercepts the fundamental operations on an object — get, set, has, delete, apply, construct — through traps, so I can add behaviour around reading and writing without touching the object's call sites.
Reflectis its matched pair: it exposes the default version of each operation as a function, so inside a trap I do the normal thing withReflect.get/setand layer my logic on top.Reflectisn't cosmetic — passing thereceiverthroughReflect.getkeepsthispointing at the proxy, so getters on the prototype are still tracked, which is exactly why Vue 3's reactivity uses it; and delegating throughReflectpreserves the engine's invariants so I don't accidentally throw. The gotchas I watch:setanddeletePropertymust return a boolean or strict mode throws, traps aren't recursive so deep reactivity means wrapping nested objects on access, and proxies break identity, soproxy !== target. And I never proxy hot data — every trapped operation is a function call that defeats JIT optimisation."
🔗 Go deeper
- javascript.info — Proxy and Reflect — every trap, the invariants, and the
receiversubtlety, worked through. - MDN — Proxy — the full trap list and handler signatures.
- MDN — Reflect — why each trap has a
Reflectcounterpart with the same signature.