get fires on reads, set on writes. Code using the proxy has no idea it's not talking to the real object. That's the whole point.
Validation
You can enforce constraints at the object level. Every write goes through the set trap, so validation happens in one place regardless of who's writing.
validation-proxy.js — runtime type checking
const schema = {
name: (v) => typeof v === 'string' && v.length > 0,
age: (v) => typeof v === 'number' && v >= 0 && v <= 150,
email: (v) => typeof v === 'string' && v.includes('@'),
};
function createValidated(initial, schema) {
return new Proxy(initial, {
set(target, prop, value) {
const validator = schema[prop];
if (validator && !validator(value)) {
console.error(`Rejected: ${prop} = ${JSON.stringify(value)}`);
return false;
}
target[prop] = value;
console.log(`Accepted: ${prop} = ${JSON.stringify(value)}`);
return true;
},
});
}
const user = createValidated(
{ name: 'Alice', age: 30, email: 'alice@dev.io' },
schema
);
user.name = 'Bob';
user.age = 200; // rejected — out of range
user.email = 'not-an-email'; // rejected — no @
user.email = 'bob@dev.io';
console.log('Final:', JSON.stringify(user));
Reactive state
This is how Vue does reactivity under the hood. Property changes go through set, which notifies anyone who subscribed. ~20 lines:
reactive-proxy.js — subscribe to state changes
function reactive(obj) {
const listeners = new Map();
function on(prop, fn) {
if (!listeners.has(prop)) listeners.set(prop, []);
listeners.get(prop).push(fn);
}
const proxy = new Proxy(obj, {
set(target, prop, value) {
const old = target[prop];
target[prop] = value;
if (old !== value) {
const fns = listeners.get(prop) || [];
fns.forEach(fn => fn(value, old));
}
return true;
},
});
return { state: proxy, on };
}
const { state, on } = reactive({ count: 0, label: 'clicks' });
on('count', (val, old) => console.log(`count: ${old} → ${val}`));
on('label', (val) => console.log(`label is now "${val}"`));
state.count = 1;
state.count = 2;
state.count = 3;
state.label = 'total clicks';
state.count = 3; // same value — no notification
Memoization
The apply trap intercepts function calls. Wrap a function in a proxy, cache results by arguments, return cached values on repeat calls. The caller doesn't know caching exists.
memoize-proxy.js — transparent function caching
function memoize(fn) {
const cache = new Map();
return new Proxy(fn, {
apply(target, thisArg, args) {
const key = JSON.stringify(args);
if (cache.has(key)) {
console.log(`Cache hit for ${key}`);
return cache.get(key);
}
console.log(`Computing for ${key}...`);
const result = target.apply(thisArg, args);
cache.set(key, result);
return result;
},
});
}
const fib = memoize(function fibonacci(n) {
if (n <= 1) return n;
return fib(n - 1) + fib(n - 2);
});
console.log('fib(10) =', fib(10));
console.log('fib(10) =', fib(10)); // cached
console.log('fib(5) =', fib(5)); // also cached from earlier
Access control
Make properties read-only, hide fields from Object.keys(), block access to sensitive data. All without touching the original object.
Instead of obj.prop ?? fallback everywhere, make the object itself return defaults for missing keys.
defaults-proxy.js — automatic fallbacks
function withDefaults(obj, defaults) {
return new Proxy(obj, {
get(target, prop) {
if (prop in target) return target[prop];
if (prop in defaults) {
console.log(`Using default for "${prop}"`);
return defaults[prop];
}
return undefined;
},
});
}
const settings = withDefaults(
{ theme: 'dark' },
{ theme: 'light', language: 'en', fontSize: 14, showLineNumbers: true }
);
console.log('theme:', settings.theme); // from object
console.log('language:', settings.language); // default
console.log('fontSize:', settings.fontSize); // default
console.log('showLineNumbers:', settings.showLineNumbers); // default
console.log('unknown:', settings.somethingElse); // undefined
All traps
The examples above use get, set, apply, and ownKeys. Here's everything you can intercept:
Trap
Intercepts
Triggered by
get
Property read
proxy.name
set
Property write
proxy.name = 'x'
has
in operator
'name' in proxy
deleteProperty
Deletion
delete proxy.name
apply
Function call
proxy(args)
construct
new
new proxy()
ownKeys
Key enumeration
Object.keys(proxy)
getOwnPropertyDescriptor
Descriptor lookup
Object.getOwnPropertyDescriptor()
defineProperty
Property definition
Object.defineProperty()
getPrototypeOf
Prototype read
Object.getPrototypeOf()
setPrototypeOf
Prototype write
Object.setPrototypeOf()
isExtensible
Extensibility check
Object.isExtensible()
preventExtensions
Lock object
Object.preventExtensions()
Trade-offs
Performance — every trapped operation has overhead. Don't wrap objects in hot loops.
Debugging — stack traces include the proxy layer. DevTools shows the proxy wrapper, not the original object.
Equality — proxy !== target. Strict equality checks against the original break.
No polyfill — Proxies can't be polyfilled. Old browsers are out.
Use them at system boundaries — config, API layers, state management — where the interception cost is negligible compared to the actual work being done.