Proxy and Reflect in JavaScript
JavaScript’s Proxy and Reflect APIs give you interception powers over object operations. Introduced in ES6, these features let you intercept and customize fundamental object behaviors like property access, assignment, deletion, and function invocation. If you’ve ever wanted to add validation automatically, create lazy-loading properties, or build reactive data structures, Proxy is the tool.
What Is a Proxy?
A Proxy wraps an object and intercepts operations that would normally happen directly on that object. You define a handler object with “traps” — functions that get called when specific operations occur.
const target = {
name: 'Alice',
age: 30
};
const handler = {
get(target, prop, receiver) {
console.log(`Getting ${prop}`);
return target[prop];
}
};
const proxy = new Proxy(target, handler);
console.log(proxy.name); // Logs: Getting name, then returns "Alice"
console.log(proxy.age); // Logs: Getting age, then returns 30
The handler receives three arguments: the target object, the property being accessed, and the receiver (which is the proxy itself unless inherited). This basic pattern opens up significant possibilities.
The get() Trap
The get trap fires whenever you read a property. It’s useful for creating computed properties, providing defaults, or implementing lazy initialization.
const user = {
firstName: 'John',
lastName: 'Doe'
};
const proxy = new Proxy(user, {
get(target, prop) {
if (prop === 'fullName') {
return `${target.firstName} ${target.lastName}`;
}
return target[prop];
}
});
console.log(proxy.firstName); // "John"
console.log(proxy.fullName); // "John Doe" - computed on access
A common pattern is returning a default value for non-existent properties:
const config = new Proxy({}, {
get(target, prop) {
if (!(prop in target)) {
console.warn(`Configuration "${prop}" not set, using default`);
return 'default';
}
return target[prop];
}
});
console.log(config.databaseUrl); // Logs warning, returns "default"
config.databaseUrl = 'postgres://localhost';
console.log(config.databaseUrl); // "postgres://localhost"
The set() Trap
The set trap intercepts property assignments. This is ideal for validation, computed properties, or maintaining relationships between properties.
const person = {};
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' && !value.includes('@')) {
throw new Error('Invalid email format');
}
target[prop] = value;
return true;
}
};
const proxy = new Proxy(person, validator);
proxy.age = 30; // Works
proxy.email = 'test@example.com'; // Works
proxy.age = 'thirty'; // Throws TypeError
proxy.age = 200; // Throws RangeError
You can also use set to create two-way bindings:
const state = {
_value: 0,
get value() { return this._value; },
set value(v) { this._value = v; }
};
const handlers = {
set(target, prop, value) {
target[prop] = value;
console.log(`State changed: ${prop} = ${value}`);
return true;
}
};
const proxy = new Proxy(state, handlers);
proxy.value = 42; // Logs: State changed: value = 42
The has() Trap
The has trap controls what happens with the in operator. You can hide properties or compute presence dynamically.
const api = {
_secret: 'api-key-123',
endpoints: ['users', 'posts', 'comments']
};
const proxy = new Proxy(api, {
has(target, prop) {
if (prop.startsWith('_')) {
return false; // Hide private properties
}
return prop in target;
}
});
console.log('endpoints' in proxy); // true
console.log('_secret' in proxy); // false - hidden
The deleteProperty() Trap
Use this to intercept the delete operator. You can prevent deletion of protected properties or log deletions.
const protectedObj = {
name: 'Protected',
_internal: 'do not delete'
};
const proxy = new Proxy(protectedObj, {
deleteProperty(target, prop) {
if (prop.startsWith('_')) {
console.warn(`Cannot delete protected property: ${prop}`);
return false;
}
delete target[prop];
return true;
}
});
delete proxy.name; // Works, deletes name
delete proxy._internal; // Logs warning, returns false
The apply() Trap
The apply trap intercepts function calls. This enables function wrapping, argument transformation, and debouncing.
function multiply(a, b) {
return a * b;
}
const traced = new Proxy(multiply, {
apply(target, thisArg, args) {
console.log(`Calling ${target.name} with args:`, args);
const result = target.apply(thisArg, args);
console.log(`Result: ${result}`);
return result;
}
});
const result = traced(3, 4);
// Logs: Calling multiply with args: [3, 4]
// Logs: Result: 12
// Returns: 12
You can also use apply to implement function composition or memoization:
const memoize = (fn) => {
const cache = new Map();
return new Proxy(fn, {
apply(target, thisArg, args) {
const key = JSON.stringify(args);
if (cache.has(key)) {
return cache.get(key);
}
const result = target.apply(thisArg, args);
cache.set(key, result);
return result;
}
});
};
const expensive = memoize((n) => {
console.log('Computing...');
return n * n;
});
console.log(expensive(5)); // Logs "Computing...", returns 25
console.log(expensive(5)); // Returns 25 (cached)
The construct() Trap
The construct trap handles the new operator. Use it to intercept object creation, validate arguments, or create factory patterns.
class Builder {
constructor(type) {
this.type = type;
}
}
const proxy = new Proxy(Builder, {
construct(target, args) {
console.log(`Creating instance with:`, args);
return new target(...args);
}
});
const instance = new proxy('custom');
// Logs: Creating instance with: ["custom"]
// instance is a Builder instance with type = "custom"
The ownKeys() Trap
The ownKeys trap controls what properties show up in Object.keys(), for...in, and Object.getOwnPropertyNames(). Hide certain properties from enumeration.
const sensitiveData = {
username: 'john',
password: 'secret123',
lastLogin: '2024-01-15'
};
const secure = new Proxy(sensitiveData, {
ownKeys(target) {
return Object.keys(target).filter(key => key !== 'password');
}
});
console.log(Object.keys(secure)); // ["username", "lastLogin"]
Reflect API
The Reflect object provides static methods that correspond to the Proxy traps. These methods give you the default behavior for each operation, which is invaluable inside trap handlers.
const target = { a: 1 };
const proxy = new Proxy(target, {
get(target, prop, receiver) {
// Custom behavior
console.log(`Accessing ${prop}`);
// Default behavior - use Reflect
return Reflect.get(target, prop, receiver);
}
});
Reflect provides these methods:
| Method | Equivalent |
|---|---|
Reflect.get(obj, prop) | obj[prop] |
Reflect.set(obj, prop, value) | obj[prop] = value |
Reflect.has(obj, prop) | prop in obj |
Reflect.deleteProperty(obj, prop) | delete obj[prop] |
Reflect.ownKeys(obj) | Object.getOwnPropertyNames(obj) |
Reflect.getPrototypeOf(obj) | Object.getPrototypeOf(obj) |
Reflect.setPrototypeOf(obj, proto) | Object.setPrototypeOf(obj, proto) |
Reflect.isExtensible(obj) | Object.isExtensible(obj) |
Reflect.preventExtensions(obj) | Object.preventExtensions(obj) |
Reflect.getOwnPropertyDescriptor(obj, prop) | Object.getOwnPropertyDescriptor(obj, prop) |
Reflect.defineProperty(obj, prop, descriptor) | Object.defineProperty(obj, prop, descriptor) |
Reflect.apply(fn, thisArg, args) | fn.apply(thisArg, args) |
Reflect.construct(fn, args) | new fn(...args) |
The key advantage of Reflect is consistency. Instead of remembering different syntaxes for each operation, you have a unified API. Inside proxy traps, always use Reflect to delegate to the default behavior — it’s cleaner and handles edge cases better.
Practical Use Cases
Observable Objects
Create objects that notify watchers when properties change:
function createObservable(target, onChange) {
const handler = {
set(obj, prop, value) {
const oldValue = obj[prop];
obj[prop] = value;
onChange(prop, oldValue, value);
return true;
}
};
return new Proxy(target, handler);
}
const state = createObservable({ count: 0 }, (prop, oldVal, newVal) => {
console.log(`${prop} changed: ${oldVal} -> ${newVal}`);
});
state.count = 1; // Logs: count changed: 0 -> 1
state.count = 5; // Logs: count changed: 1 -> 5
Revocable Proxies
Create proxies that can be disabled:
const { proxy, revoke } = Proxy.revocable({
secret: 'hidden'
}, {
get(target, prop) {
return target[prop];
}
});
console.log(proxy.secret); // "hidden"
revoke();
console.log(proxy.secret); // TypeError: Cannot perform 'get' on a revoked Proxy
Property Access Logging
Debug property access in production without modifying source code:
const debugProxy = (obj) => new Proxy(obj, {
get(target, prop, receiver) {
const value = Reflect.get(target, prop, receiver);
console.log(`GET ${String(prop)}:`, value);
return value;
},
set(target, prop, value) {
console.log(`SET ${String(prop)}:`, value);
return Reflect.set(target, prop, value);
}
});
const tracked = debugProxy({ x: 1 });
tracked.x = 2;
// Logs: GET x: 1
// Logs: SET x: 2
Invariants
Proxies enforce certain invariants that you cannot override. Violating these throws a TypeError:
- Non-configurable properties cannot be deleted
- Extensible targets cannot be made non-extensible through the proxy
- Non-writable properties cannot have their values changed
- A property cannot be made non-configurable if it doesn’t exist as a non-configurable on the target
These protections ensure that proxies maintain the basic guarantees JavaScript objects provide.
See Also
- Prototypes and Prototype Chain — Understand JavaScript’s inheritance model that Proxies interact with
- JavaScript Symbols — Another ES6 feature for creating unique property keys and metaprogramming
- Object.create() — ES5 inheritance pattern that Proxy builds upon