Proxy and Reflect Metaprogramming

Hard100% Free~30 mins#proxy#reflect#metaprogramming#reactivity#traps#invariants#modern-es6
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 = [];
3
4const 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};
28
29const proxy = new Proxy(target, handler);
30
31logs.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);
38
39console.log(logs.join(' '));
Predict Console Output
Interactive Challenge

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.

Call Stack (Top = Active)
Global Execution Context
handler.get(tier)
Lexical Scope / Bindings
prop:'tier'
logs:[]
Console Stream
> [empty]

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 Object

Why Pair Proxy with Reflect?

Always forward intercepted operations using Reflect methods rather than direct object property access (t[prop]):

  1. Receiver Preservation (this binding):
    When accessing a getter property on target, direct access (t[prop]) binds this inside the getter to target. Forwarding with Reflect.get(t, prop, receiver) correctly preserves receiver (the Proxy instance), allowing inherited getters and computed reactive dependencies to track properly.
  2. Clean Boolean Returns:
    Reflect.set() and Reflect.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 get trap must return the actual property value.
  • If a target property is non-configurable, the deleteProperty trap cannot return true.
    Violating invariants throws a runtime TypeError.
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?