Proxy and Reflect Metaprogramming
Key Learning Objectives
Master the Proxy constructor and understand how traps intercept internal ECMAScript object methods ([[Get]], [[Set]], [[HasProperty]], [[Delete]]).
Pair Proxy handlers with Reflect methods (Reflect.get, Reflect.set) to preserve receiver context and default engine behaviors.
Understand Proxy invariants: enforce that non-configurable or non-writable target properties cannot return altered values.
Compare Proxy-based reactivity (Vue 3, MobX) to Object.defineProperty: dynamic property additions, delete interception, and array index tracking.
Evaluate performance and debugging trade-offs: trap invocation overhead and hidden call-stack complexity.
The Interview Problem
What is logged to the console when the following Proxy and Reflect operations execute across get, set, has, and deleteProperty traps?
1const target = { name: 'Alice', age: 28, _secret: 'shh' };2const logs = [];34const handler = {5 get(t, prop, receiver) {6 if (prop === 'tier') return 'Gold';7 return Reflect.get(t, prop, receiver);8 },9 set(t, prop, val, receiver) {10 if (prop === 'age' && (typeof val !== 'number' || val < 0)) {11 logs.push('invalid-age');12 return false;13 }14 return Reflect.set(t, prop, val, receiver);15 },16 has(t, prop) {17 if (typeof prop === 'string' && prop.startsWith('_')) return false;18 return Reflect.has(t, prop);19 },20 deleteProperty(t, prop) {21 if (prop === 'name') {22 logs.push('cant-delete');23 return false;24 }25 return Reflect.deleteProperty(t, prop);26 }27};2829const proxy = new Proxy(target, handler);3031logs.push(proxy.tier);32proxy.age = -5;33proxy.age = 30;34logs.push(proxy.age);35logs.push('_secret' in proxy);36delete proxy.name;37logs.push(proxy.name);3839console.log(logs.join(' '));
Predict Console Output
Select the option that matches what standard ECMAScript prints to the console:
Gold invalid-age 30 false cant-delete Alice
undefined invalid-age 30 true cant-delete undefined
Gold invalid-age -5 false cant-delete Alice
Gold 30 false Alice
V8 Engine Execution Trace
Step 1 of 7 (Line 32)Accessing proxy.tier triggers handler.get. The trap matches prop === 'tier' and returns 'Gold' without inspecting the target.
Deep Technical Breakdown
Core Mental Model: Proxy & Reflect
A Proxy wraps a target object and intercepts fundamental internal ECMAScript operations (called internal methods such as [[Get]], [[Set]], [[HasProperty]], and [[Delete]]).
User Code ──► Proxy Handler (Traps) ──► Reflect (Default Engine Behavior) ──► Target ObjectWhy Pair Proxy with Reflect?
Always forward intercepted operations using Reflect methods rather than direct object property access (t[prop]):
- Receiver Preservation (
thisbinding):
When accessing a getter property ontarget, direct access (t[prop]) bindsthisinside the getter totarget. Forwarding withReflect.get(t, prop, receiver)correctly preservesreceiver(the Proxy instance), allowing inherited getters and computed reactive dependencies to track properly. - Clean Boolean Returns:
Reflect.set()andReflect.deleteProperty()return boolean success flags instead of throwing errors or returning undefined, matching trap return signatures.
Reactive State Systems (Vue 3 vs Object.defineProperty)
In Vue 2, reactivity used Object.defineProperty(). This had severe limitations:
- Could not detect dynamically added properties without
Vue.set(). - Could not intercept property deletion (
delete obj.key). - Required wrapping array mutator methods (
push,pop,splice) because indexing (arr[0] = x) was not observable.
Proxy resolves all three limitations because it operates at the object level, intercepting all dynamic property reads, writes, deletions, and enumeration.
Proxy Invariants
JavaScript engines strictly enforce Proxy Invariants to maintain language sanity:
- If a target property is non-configurable and non-writable, the
gettrap must return the actual property value. - If a target property is non-configurable, the
deletePropertytrap cannot returntrue.
Violating invariants throws a runtimeTypeError.
Common Traps & Mistakes
Using direct property access (`target[prop] = value`) inside traps instead of `Reflect.set(target, prop, value, receiver)`, causing lost receiver context in getters and prototype chains.
Forgetting that returning `false` from a `set` trap in strict mode (`'use strict'`) throws a `TypeError: 'set' on proxy: trap returned falsish for property`.
Assuming Proxies intercept private class fields (`#privateField`): accessing private fields through a Proxy throws a `TypeError` because the engine checks the raw target's private brand identity.
Ignoring performance overhead: invoking a JS trap on every property access in high-frequency loops (e.g., 60fps canvas calculations) creates significant V8 optimization deopts.
FAANG Follow-Up Probes
Probe #1
How does Vue 3 handle deeply nested reactive objects using Proxy without eagerly wrapping the entire object tree at startup?
Probe #2
Why does accessing a private class field (`this.#secret`) via a Proxy instance throw a TypeError, and how can the membrane pattern mitigate this?
Probe #3
How would you implement an observable data store that records an undo/redo mutation log using a Proxy `set` trap?
