Proxy Basics
new Proxy(target, handler) creates a proxy for target. The handler object contains traps β methods named after the operations they intercept. If a trap is absent, the operation falls through to the target unmodified.
const target = { name: 'Alice', age: 30 };
const handler = {
get(target, prop, receiver) {
console.log(`GET ${String(prop)}`);
return Reflect.get(target, prop, receiver); // default behaviour
},
set(target, prop, value, receiver) {
console.log(`SET ${String(prop)} = ${value}`);
return Reflect.set(target, prop, value, receiver);
}
};
const proxy = new Proxy(target, handler);
console.log(proxy.name); // triggers get trap
proxy.age = 31; // triggers set trap
console.log(proxy.age); // triggers get trap
// The target is also mutated
console.log(target.age); // 31
GET name Alice SET age = 31 GET age 31 31
Proxy for Validation
The set trap can enforce type and value constraints on every property write, turning any plain object into a validated schema.
function createValidated(schema) {
return new Proxy({}, {
set(target, prop, value) {
const rule = schema[prop];
if (!rule) throw new TypeError(`Unknown property: ${String(prop)}`);
if (rule.type && typeof value !== rule.type) {
throw new TypeError(`${String(prop)} must be ${rule.type}, got ${typeof value}`);
}
if (rule.min !== undefined && value < rule.min) {
throw new RangeError(`${String(prop)} must be >= ${rule.min}`);
}
if (rule.max !== undefined && value > rule.max) {
throw new RangeError(`${String(prop)} must be <= ${rule.max}`);
}
if (rule.pattern && !rule.pattern.test(value)) {
throw new Error(`${String(prop)} does not match pattern`);
}
return Reflect.set(target, prop, value);
}
});
}
const user = createValidated({
name: { type: 'string', pattern: /^[A-Za-z ]{2,50}$/ },
age: { type: 'number', min: 0, max: 150 },
email: { type: 'string', pattern: /^.+@.+\..+$/ }
});
user.name = 'Bob Smith';
user.age = 25;
user.email = 'bob@example.com';
console.log(user.name, user.age, user.email);
try { user.age = -5; } catch (e) { console.error(e.message); }
try { user.name = 123; } catch (e) { console.error(e.message); }
try { user.phone = '555'; } catch (e) { console.error(e.message); }
Bob Smith 25 bob@example.com age must be >= 0 name must be string, got number Unknown property: phone
Other Useful Traps
const sensitiveData = {
password: 'secret',
apiKey: 'abc123',
name: 'Alice'
};
const secureProxy = new Proxy(sensitiveData, {
// Intercept property read
get(target, prop) {
if (prop === 'password' || prop === 'apiKey') {
return '***REDACTED***';
}
return Reflect.get(target, prop);
},
// Intercept `in` operator
has(target, prop) {
console.log(`Checking: ${String(prop)} in proxy`);
return Reflect.has(target, prop);
},
// Intercept delete operator
deleteProperty(target, prop) {
if (prop === 'password') {
throw new Error('Cannot delete password field');
}
return Reflect.deleteProperty(target, prop);
},
// Intercept Object.keys / for...in
ownKeys(target) {
return Reflect.ownKeys(target).filter(k => k !== 'apiKey');
}
});
console.log(secureProxy.password); // ***REDACTED***
console.log(secureProxy.name); // Alice
console.log('password' in secureProxy); // true (but value is hidden)
console.log(Object.keys(secureProxy)); // ['password', 'name'] (no apiKey)
try { delete secureProxy.password; } catch (e) { console.error(e.message); }
***REDACTED*** Alice Checking: password in proxy true ['password', 'name'] Cannot delete password field
The Reflect API
| Reflect method | Equivalent operation | Used in trap |
|---|---|---|
Reflect.get(t, p, r) | t[p] | get |
Reflect.set(t, p, v, r) | t[p] = v | set |
Reflect.has(t, p) | p in t | has |
Reflect.deleteProperty(t, p) | delete t[p] | deleteProperty |
Reflect.apply(t, th, a) | t.apply(th, a) | apply |
Reflect.construct(t, a, nt) | new t(...a) | construct |
Reflect.ownKeys(t) | Object.getOwnPropertyNames + Symbols | ownKeys |
Inside a proxy trap, always forward to the corresponding Reflect method to perform the default behaviour. Doing the equivalent operation on target directly (target[prop]) bypasses getter/setter semantics and can break object invariants. Reflect methods also return the correct boolean for traps like set and deleteProperty.
Default Values and Function Call Trapping
// Default value proxy (like Python's defaultdict)
function withDefaults(target, defaultValue) {
return new Proxy(target, {
get(obj, prop) {
return prop in obj ? obj[prop] : defaultValue;
}
});
}
const scores = withDefaults({}, 0);
scores.Alice = 10;
scores.Bob = 20;
console.log(scores.Alice); // 10
console.log(scores.Charlie); // 0 (default β not set)
// Function call trap
function multiply(a, b) { return a * b; }
const trackedMultiply = new Proxy(multiply, {
apply(target, thisArg, args) {
console.log(`Called with args: ${args}`);
const result = Reflect.apply(target, thisArg, args);
console.log(`Result: ${result}`);
return result;
}
});
trackedMultiply(3, 4);
trackedMultiply(7, 8);
10 0 Called with args: 3,4 Result: 12 Called with args: 7,8 Result: 56
Proxy.revocable()
Proxy.revocable(target, handler) returns { proxy, revoke }. After calling revoke(), any operation on the proxy throws a TypeError. Useful for temporary access tokens or capability objects.
function createTemporaryAccess(data, ttlMs) {
const { proxy, revoke } = Proxy.revocable(data, {});
setTimeout(revoke, ttlMs); // auto-revoke after TTL
return proxy;
}
const tempAccess = createTemporaryAccess({ secret: 'TOKEN_XYZ' }, 500);
console.log(tempAccess.secret); // 'TOKEN_XYZ'
setTimeout(() => {
try {
console.log(tempAccess.secret); // throws after 500ms
} catch (e) {
console.error('Access revoked:', e.message);
}
}, 600);
TOKEN_XYZ Access revoked: Cannot perform 'get' on a proxy that has been revoked
Vue 3's reactivity system (reactive(), ref()) wraps your state objects in Proxies with get and set traps. The get trap tracks which component is reading the value; the set trap triggers re-renders when the value changes. This is fundamentally more capable than Vue 2's Object.defineProperty approach, which couldn't detect property additions or deletions.
ποΈ Practical Exercise
Build a createObservable(obj) function that:
- Returns a proxied version of
obj. - Accepts subscriber callbacks via
observable.on(prop, callback). - Calls
callback(newValue, oldValue)wheneverpropchanges. - Supports nested objects (deep proxy).
- Test: observe a user object and log changes to
nameandemail.
π₯ Challenge Exercise
Implement a lightweight reactive(obj) system inspired by Vue 3: when a property changes, automatically re-run any "effect" functions that previously read that property. Use a global activeEffect variable to track the currently-running effect, a get trap to subscribe it, and a set trap to trigger subscribers. Demonstrate with a computed display name that auto-updates when firstName or lastName changes.
Interview Questions
- What is a Proxy in JavaScript and what problem does it solve?
- Name five proxy traps and describe what operation each intercepts.
- Why should you use Reflect methods inside proxy traps instead of operating on the target directly?
- What is Proxy.revocable() and when would you use it?
- How does Vue 3 use Proxy for its reactivity system?
π Summary
new Proxy(target, handler)intercepts operations on the target via trap methods.- Common traps:
get,set,has,deleteProperty,apply,ownKeys. - Always forward to
Reflectinside traps to maintain correct semantics. - Use Proxy for validation, logging, default values, read-only access, and observability.
Proxy.revocable()creates proxies that can be disabled, enabling time-limited access tokens.- Vue 3's reactivity system is built on Proxy β a real-world example of production metaprogramming.
Frequently Asked Questions
No. Private class fields (#field) are stored in an internal slot, not as regular object properties. Proxy traps only intercept regular property access. A get trap on a proxy of a class instance will not see reads to private fields β those bypass the proxy entirely.
Object.defineProperty getters are tied to a specific named property on a specific object β you must define one per property. A Proxy get trap intercepts all property reads on the target, including dynamic and unknown property names. This makes Proxy much more flexible for generic patterns like default values or logging all accesses.
Yes β proxy traps add overhead to every intercepted operation. Modern JS engines have optimised this considerably, but Proxies are still slower than direct property access. In hot code paths, profile before proxying. For UI reactivity (Vue, MobX), the overhead is negligible compared to rendering costs.