Proxies power reactivity systems, validation layers, mocks and API wrappers.
A Proxy wraps a target object and lets you intercept fundamental operations on it through traps defined in a handler:
const p = new Proxy(target, {
get(target, key, receiver) { ... },
set(target, key, value, receiver) { ... },
});There are traps for every internal operation: get, set, has (in), deleteProperty, ownKeys (Object.keys, for...in), getOwnPropertyDescriptor, defineProperty, getPrototypeOf, and for function targets apply (calls) and construct (new). Any trap you omit falls through to the target.
Reflect has a static method with the same name and signature for each trap. Use it to perform the default behaviour inside a trap: return Reflect.get(target, key, receiver). Passing receiver matters: it makes getters on the target (or its prototype chain) run with this set to the proxy, so accesses inside them are intercepted too. Reflect.set and friends also return booleans instead of throwing, which is exactly what traps must return.
Common uses:
- Validation: reject bad writes in
set. - Reactivity: record reads in
get, trigger updates inset— Vue 3's reactivity is built this way. - Defaults / negative indexes / logging / mocks for tests.
Limits and costs:
- The engine enforces invariants: e.g. a trap can't report a non-configurable property as missing; violations throw
TypeError. - A proxy is a different object:
proxy !== target. - Objects with internal slots break through proxies:
Mapmethods and class#privatefields fail whenthisis the proxy. Wrap the target's methods or avoid proxying such objects. - Every operation goes through a function call, so proxies are slower than plain objects in hot paths.
Example
function validated(target, schema) {
return new Proxy(target, {
set(obj, key, value, receiver) {
const check = schema[key];
if (check && !check(value)) throw new TypeError(`Invalid ${String(key)}: ${value}`);
return Reflect.set(obj, key, value, receiver);
},
get(obj, key, receiver) {
if (!(key in obj)) console.log(`(read of missing ${String(key)})`);
return Reflect.get(obj, key, receiver);
},
});
}
const user = validated({ name: "Ada", age: 36 }, {
age: v => Number.isInteger(v) && v >= 0,
});
user.age = 37;
try { user.age = -5; } catch (e) { console.log(e.message); } // Invalid age: -5
console.log(user.age); // 37
user.email; // (read of missing email)
const m = new Proxy(new Map(), {});
try { m.set("k", 1); } catch (e) { console.log(e.constructor.name); } // TypeErrorRemember
- A Proxy intercepts operations via traps; missing traps forward to the target
- Use
Reflect.*inside traps, passingreceiver, for correct default behaviour - Proxies break on internal slots (Map, #private) and cost performance
Watch out for
- Returning nothing from a
settrap: in strict mode a falsy result throws TypeError.