Proxy & Reflect
Intercept and redefine fundamental object operations using Proxy traps, and maintain correct semantics with the Reflect API.
Overview
Key Points
- •new Proxy(target, handler) creates a proxy; operations on the proxy are intercepted by handler traps
- •The 13 proxy traps correspond to internal methods: get, set, has, deleteProperty, apply, construct, and more
- •Reflect provides a method for every trap always use Reflect inside traps to forward operations correctly
- •The receiver parameter in get/set traps is crucial for prototype chains pass it to Reflect to preserve this
- •A Proxy can be revoked with Proxy.revocable() after revocation, any access throws a TypeError
- •Traps for non-configurable, non-writable properties have invariants the runtime enforces violating them throws
- •Proxies are transparent: instanceof, typeof, and the original object's prototype chain all work through them
- •set traps must return true (success) or false/throw in strict mode forgetting this causes silent failures
Examples
Validation proxy enforce property types on a schema
function createValidated(schema) {
return new Proxy({}, {
set(target, prop, value, receiver) {
if (prop in schema) {
const expected = schema[prop];
if (typeof value !== expected) {
throw new TypeError(
`Property "${prop}" must be ${expected}, got ${typeof value}`
);
}
}
return Reflect.set(target, prop, value, receiver);
},
get(target, prop, receiver) {
const val = Reflect.get(target, prop, receiver);
return val === undefined && prop in schema ? null : val;
}
});
}
const user = createValidated({ name: 'string', age: 'number' });
user.name = 'Alice'; // OK
user.age = 30; // OK
try {
user.age = '30'; // TypeError: Property "age" must be number, got string
} catch (e) {
console.error(e.message);
}
console.log(user.name); // Alice
console.log(user.score); // null (not in schema but handled)Reflect.set returns a boolean indicating success. Always return it from the set trap so non-writable properties still throw in strict mode.
Reactive object with change tracking (simplified Vue 3 pattern)
function reactive(target, onChange) {
const handler = {
get(obj, key, receiver) {
const value = Reflect.get(obj, key, receiver);
// Recursively wrap nested objects
if (value !== null && typeof value === 'object') {
return reactive(value, onChange);
}
return value;
},
set(obj, key, value, receiver) {
const oldValue = obj[key];
const result = Reflect.set(obj, key, value, receiver);
if (result && oldValue !== value) {
onChange(key, oldValue, value);
}
return result;
}
};
return new Proxy(target, handler);
}
const state = reactive({ count: 0, user: { name: 'Bob' } }, (key, from, to) => {
console.log(`${key}: ${from} -> ${to}`);
});
state.count++; // count: 0 -> 1
state.user.name = 'Alice'; // name: Bob -> AliceBecause nested objects are wrapped lazily (only when accessed), this avoids the overhead of deep upfront proxying.
Revocable proxy and negative-index array access
// Revocable proxy security / session tokens
const { proxy: secret, revoke } = Proxy.revocable(
{ token: 'abc123', expiry: Date.now() + 60000 },
{}
);
console.log(secret.token); // abc123
revoke();
try {
console.log(secret.token); // TypeError: Cannot perform 'get' on a revoked proxy
} catch (e) {
console.error(e.message);
}
// Negative array indexing
function negativeIndex(arr) {
return new Proxy(arr, {
get(target, prop, receiver) {
const idx = Number(prop);
if (!isNaN(idx) && idx < 0) {
return Reflect.get(target, target.length + idx, receiver);
}
return Reflect.get(target, prop, receiver);
}
});
}
const a = negativeIndex([10, 20, 30, 40]);
console.log(a[-1]); // 40
console.log(a[-2]); // 30
console.log(a.length); // 4Proxy.revocable() is ideal for capability objects hand out a proxy, then revoke access when the session or permission expires.
Common Mistakes
- Omitting the receiver argument when calling Reflect.get/set this breaks prototype-chain behaviour when a getter or setter is inherited
- Forgetting to return true from the set trap in non-strict mode this silently fails; in strict mode it throws a TypeError
- Trying to violate property descriptor invariants (returning a different value for a non-writable, non-configurable property) the runtime throws even if your trap tries to lie
- Expecting Proxy to make an object deeply reactive automatically nested objects must be wrapped individually, typically lazily on access
Note
Always pair Proxy with Reflect: for every trap foo, call Reflect.foo(...arguments) as the default forwarding path. This ensures your proxy is transparent for all operations you do not intentionally override, and correctly handles edge cases in the spec like non-configurable property invariants.
Exercises
Write JavaScript code to solve each exercise. Run it and check your output.
Completed: 0 / 5 exercises
Knowledge Check
Question 1 of 4