Proxy & Reflect: Trapping [[Get]], [[Set]], and Engine Invariants
- Last updated
- Prerequisites:
- JavaScript object property semantics
- Understanding of getters/setters and descriptors
- proxy
- reflect
- meta-programming
- internal-methods
- traps
- invariants
Read at your depth
The practical view
A Proxy wraps an object (the target) and lets you intercept operations via traps: get, set, has, deleteProperty, ownKeys, apply, construct, and more. const p = new Proxy(target, { get(obj, key) { return key in obj ? obj[key] : 'missing'; } });. Reflect provides the default behavior as plain functions: Reflect.get(target, key) does the standard lookup, and calling it inside a trap avoids infinite recursion. Use Proxies for validation, logging, observability (like Vue's reactivity), memoization, and mocking. Note: a proxy does NOT change the target — traps are added behavior around it. Proxies are slower than plain objects, so use them where interception is the feature, not on every hot path.
The same idea in other frameworks
React dev tools and state libs use proxies to instrument component state; production hot paths avoid proxies for speed.Zone.js patches rather than proxies most internals, but Angular's zoneless experiments use signal-based (non-proxy) reactivity.Vue 3 reactivity IS proxy-based: reactive() wraps objects and intercepts get/set/has/ownKeys to track and trigger effects.Svelte 5 runes use compiler-driven signals rather than proxies — compiled access, not runtime interception.Legacy vs modern
Naive forwarding trap vs correct Reflect-forwarding trap with receiver
Hand-rolling get traps with target[key] breaks getter receiver binding; forwarding through Reflect preserves correct this semantics.
get(target, key) { return target[key]; } // ❌ OLD: breaks getter `this` — getters see target, not proxyget(target, key, receiver) { return Reflect.get(target, key, receiver); } // ✅ NEW: getters see the proxyInterview gotchas
Context
This probes your understanding of proxy invariants — the hard contract that keeps proxies from corrupting the object model and lets engines trust object metadata.
The mechanical answer
A non-configurable, non-writable own data property on the target is fixed: no operation may change or conceal its value without violating the object's invariant. The get trap's result is checked against the target: the engine calls [[Get]] on the target, and if the property is a non-configurable, non-writable data property, the trap must return exactly that value. Violating this throws a TypeError. This invariant protects engine invariants (e.g. shape/Map assumptions) and guarantees that a proxy cannot masquerade as having a different value for a permanently fixed property, which would otherwise break spec-guaranteed behaviors like constant folding.
Trap
The naive answer is 'the proxy can return anything it wants because it is a proxy'. Invariants forbid lying about non-configurable properties, including non-configurable accessors (get must be undefined if no setter), ownKeys must include non-configurable keys, and getPrototypeOf must return the target's actual non-extensible prototype. Another trap: assuming the invariant check is optional — engines enforce it with a TypeError, so violating it is a hard crash, not a silent fallback.
Context
Interviewers ask this to see whether you understand receiver semantics and why naive trap forwarding is subtly broken for accessors.
The mechanical answer
Reflect.get(target, key, receiver) executes the exact [[Get]] internal method with an explicit receiver — the `this` used when evaluating an accessor (getter) found during the lookup. A bare target[key] inside a get trap performs a plain property access with this bound to target, so getters run against the raw target instead of the proxy: proxy-internal state is invisible to them, and any chained property reads bypass your traps. Forwarding receiver is essential for correct proxy composition — Vue's reactive, for example, forwards receiver so nested getters observe the reactive wrapper. For data properties there is no difference; the difference only manifests with accessors and inherited getters.
Trap
The naive answer is 'they are identical for property reads'. They diverge precisely when an accessor is encountered on the prototype or target. Another trap: forgetting that Reflect functions exist for every trap and are the canonical forwarder — hand-rolling forwarding with target[key] or target[key] = v subtly breaks receiver/this, strict mode, and invariant checks. Candidates who reach for Reflect.* by default show they know the contract.