Difficulty: Advanced
The Proxy object in JavaScript enables you to create a wrapper around another object that can intercept and redefine fundamental operations such as property lookup, assignment, enumeration, and function invocation. Introduced in ES6, proxies are one of the most powerful meta-programming features in the language and form the backbone of reactivity systems in modern frameworks like Vue.js 3.
A Proxy is created using the constructor new Proxy(target, handler), where target is the original object you want to wrap and handler is an object containing "traps" - methods that intercept operations on the target. There are 13 available traps including get, set, has, deleteProperty, apply, construct, ownKeys, and more. Each trap corresponds to an internal object operation defined by the ECMAScript specification.
The Reflect API was introduced alongside Proxy and provides methods that mirror every proxy trap. Reflect.get(), Reflect.set(), Reflect.has(), and so on perform the default behavior for each trap. Using Reflect inside your trap handlers is considered best practice because it ensures you maintain the correct behavior while adding your custom logic on top. For example, calling Reflect.set(target, prop, value, receiver) in a set trap performs the default property assignment while letting you add validation or logging.
One of the most common use cases for proxies is validation. By intercepting the set trap, you can enforce type constraints, range checks, or schema validation before any value is actually written to the object. Another powerful pattern is using the get trap for logging, auto-populating defaults, or creating "virtual" properties that are computed on the fly.
Revocable proxies, created with Proxy.revocable(target, handler), return both the proxy and a revoke function. Once revoke() is called, any further interaction with the proxy throws a TypeError. This is useful for granting temporary access to an object that can be completely cut off later, which is valuable in security-sensitive contexts.
Frameworks leverage proxies extensively. Vue.js 3's entire reactivity system is built on Proxy, replacing the Object.defineProperty approach used in Vue 2. This allows Vue to detect property additions and deletions, array mutations, and nested object changes automatically - something that was impossible with the older approach.
const user = { name: 'Alice', age: 25 };
const handler = {
get(target, prop, receiver) {
console.log(`Reading property "${prop}"`);
return Reflect.get(target, prop, receiver);
},
set(target, prop, value, receiver) {
console.log(`Setting "${prop}" to ${value}`);
return Reflect.set(target, prop, value, receiver);
}
};
const proxy = new Proxy(user, handler);
console.log(proxy.name);
proxy.age = 26;
console.log(proxy.age);
Every property access triggers the get trap and every assignment triggers the set trap. Reflect methods are used to perform the default operation after our custom logging.
const validator = {
set(target, prop, value) {
if (prop === 'age') {
if (typeof value !== 'number') {
throw new TypeError('Age must be a number');
}
if (value < 0 || value > 150) {
throw new RangeError('Age must be between 0 and 150');
}
}
if (prop === 'email') {
if (typeof value !== 'string' || !value.includes('@')) {
throw new TypeError('Invalid email address');
}
}
return Reflect.set(target, prop, value);
}
};
const person = new Proxy({}, validator);
person.age = 25; // OK
person.email = 'a@b.com'; // OK
console.log(person.age, person.email);
try {
person.age = -5;
} catch (e) {
console.log(e.message);
}
try {
person.email = 'invalid';
} catch (e) {
console.log(e.message);
}
The set trap validates values before they are assigned. Invalid values throw descriptive errors without ever reaching the underlying object.
const secrets = { password: 'abc123', apiKey: 'xyz789', name: 'App' };
const hidden = ['password', 'apiKey'];
const handler = {
has(target, prop) {
if (hidden.includes(prop)) return false;
return Reflect.has(target, prop);
},
deleteProperty(target, prop) {
if (hidden.includes(prop)) {
throw new Error(`Cannot delete protected property "${prop}"`);
}
return Reflect.deleteProperty(target, prop);
},
ownKeys(target) {
return Reflect.ownKeys(target).filter(k => !hidden.includes(k));
}
};
const safe = new Proxy(secrets, handler);
console.log('password' in safe);
console.log('name' in safe);
console.log(Object.keys(safe));
try {
delete safe.password;
} catch (e) {
console.log(e.message);
}
The has trap hides sensitive properties from 'in' checks, ownKeys hides them from Object.keys(), and deleteProperty prevents their removal. The actual data still exists on the target but is inaccessible through the proxy.
const data = { value: 42 };
const { proxy, revoke } = Proxy.revocable(data, {
get(target, prop) {
return Reflect.get(target, prop);
}
});
console.log(proxy.value);
revoke();
try {
console.log(proxy.value);
} catch (e) {
console.log(e.constructor.name + ': ' + e.message);
}
Proxy.revocable returns a proxy and a revoke function. After calling revoke(), any operation on the proxy throws a TypeError. This provides a clean way to cut off access to an object.
Proxy, Reflect, Traps, Meta-programming, Validation, Reactivity