Docs
13.6-Proxy-Reflect
13.6 Proxy and Reflect
Overview
Proxy and Reflect are powerful metaprogramming features introduced in ES6 that allow you to intercept and customize fundamental operations on objects. Proxy creates a wrapper around an object to intercept operations, while Reflect provides methods that correspond to the Proxy traps.
Learning Objectives
- •Understand the Proxy API and its use cases
- •Master the Reflect API and its relationship to Proxy
- •Implement common patterns using Proxy (validation, logging, etc.)
- •Create reactive systems and observable objects
- •Understand performance implications
The Proxy Object
Basic Syntax
const proxy = new Proxy(target, handler);
- •target: The original object to wrap
- •handler: Object with trap methods that intercept operations
Simple Example
const target = { name: 'John', age: 30 };
const handler = {
get(target, property, receiver) {
console.log(`Getting ${property}`);
return target[property];
},
set(target, property, value, receiver) {
console.log(`Setting ${property} to ${value}`);
target[property] = value;
return true;
},
};
const proxy = new Proxy(target, handler);
proxy.name; // Logs: "Getting name", returns "John"
proxy.age = 31; // Logs: "Setting age to 31"
Proxy Traps
get() - Property Access
const handler = {
get(target, property, receiver) {
if (property in target) {
return target[property];
}
return `Property ${property} not found`;
},
};
const obj = new Proxy({ name: 'John' }, handler);
console.log(obj.name); // "John"
console.log(obj.missing); // "Property missing not found"
set() - Property Assignment
const handler = {
set(target, property, value, receiver) {
if (property === 'age' && typeof value !== 'number') {
throw new TypeError('Age must be a number');
}
if (property === 'age' && (value < 0 || value > 150)) {
throw new RangeError('Age must be between 0 and 150');
}
target[property] = value;
return true; // Must return true for successful set
},
};
const person = new Proxy({}, handler);
person.name = 'John'; // OK
person.age = 30; // OK
person.age = 'thirty'; // TypeError
person.age = -5; // RangeError
has() - The 'in' Operator
const handler = {
has(target, property) {
if (property.startsWith('_')) {
return false; // Hide private properties
}
return property in target;
},
};
const obj = new Proxy({ name: 'John', _secret: 'hidden' }, handler);
console.log('name' in obj); // true
console.log('_secret' in obj); // false
deleteProperty() - Property Deletion
const handler = {
deleteProperty(target, property) {
if (property.startsWith('_')) {
throw new Error('Cannot delete private properties');
}
delete target[property];
return true;
},
};
const obj = new Proxy({ name: 'John', _id: 123 }, handler);
delete obj.name; // OK
delete obj._id; // Error
apply() - Function Calls
const handler = {
apply(target, thisArg, argumentsList) {
console.log(`Calling function with args: ${argumentsList}`);
return target.apply(thisArg, argumentsList);
},
};
const sum = new Proxy((a, b) => a + b, handler);
console.log(sum(1, 2)); // Logs call info, returns 3
construct() - new Operator
const handler = {
construct(target, argumentsList, newTarget) {
console.log(`Creating new instance with args: ${argumentsList}`);
return new target(...argumentsList);
},
};
class Person {
constructor(name) {
this.name = name;
}
}
const ProxiedPerson = new Proxy(Person, handler);
const john = new ProxiedPerson('John'); // Logs construction
ownKeys() - Object.keys, etc.
const handler = {
ownKeys(target) {
// Filter out private properties
return Reflect.ownKeys(target).filter((key) => !key.startsWith('_'));
},
};
const obj = new Proxy({ name: 'John', age: 30, _secret: 'hidden' }, handler);
console.log(Object.keys(obj)); // ['name', 'age']
getOwnPropertyDescriptor() - Property Descriptors
const handler = {
getOwnPropertyDescriptor(target, property) {
if (property.startsWith('_')) {
return undefined; // Hide private properties
}
return Object.getOwnPropertyDescriptor(target, property);
},
};
The Reflect API
Reflect provides methods corresponding to Proxy traps with consistent behavior:
// Instead of:
target[property];
// Use:
Reflect.get(target, property, receiver);
// Instead of:
target[property] = value;
// Use:
Reflect.set(target, property, value, receiver);
// Instead of:
delete target[property];
// Use:
Reflect.deleteProperty(target, property);
// Instead of:
property in target;
// Use:
Reflect.has(target, property);
// Instead of:
Object.keys(target);
// Use:
Reflect.ownKeys(target);
Why Use Reflect?
// 1. Consistent return values (boolean for success/failure)
const success = Reflect.set(target, 'name', 'John');
// 2. Proper receiver handling for inheritance
const handler = {
get(target, property, receiver) {
// receiver is the proxy, not target - important for inheritance
return Reflect.get(target, property, receiver);
},
};
// 3. Functional alternative to operators
Reflect.apply(fn, thisArg, args); // fn.apply(thisArg, args)
Reflect.construct(Class, args); // new Class(...args)
Practical Patterns
Validation Proxy
function createValidatedObject(schema) {
return new Proxy(
{},
{
set(target, property, value) {
const validator = schema[property];
if (validator && !validator(value)) {
throw new TypeError(`Invalid value for ${property}`);
}
target[property] = value;
return true;
},
}
);
}
const user = createValidatedObject({
name: (v) => typeof v === 'string' && v.length > 0,
age: (v) => typeof v === 'number' && v >= 0 && v <= 150,
email: (v) => /^.+@.+\..+$/.test(v),
});
user.name = 'John'; // OK
user.age = 30; // OK
user.email = 'john@example.com'; // OK
user.age = -5; // TypeError
Observable Object
function createObservable(target, onChange) {
return new Proxy(target, {
set(target, property, value, receiver) {
const oldValue = target[property];
const result = Reflect.set(target, property, value, receiver);
if (result && oldValue !== value) {
onChange(property, oldValue, value);
}
return result;
},
deleteProperty(target, property) {
const oldValue = target[property];
const result = Reflect.deleteProperty(target, property);
if (result) {
onChange(property, oldValue, undefined);
}
return result;
},
});
}
const state = createObservable({ count: 0 }, (prop, oldVal, newVal) => {
console.log(`${prop} changed from ${oldVal} to ${newVal}`);
});
state.count = 1; // "count changed from 0 to 1"
state.count = 2; // "count changed from 1 to 2"
Negative Array Indices
function createNegativeArray(arr) {
return new Proxy(arr, {
get(target, property, receiver) {
const index = Number(property);
if (!isNaN(index) && index < 0) {
property = String(target.length + index);
}
return Reflect.get(target, property, receiver);
},
});
}
const arr = createNegativeArray([1, 2, 3, 4, 5]);
console.log(arr[-1]); // 5
console.log(arr[-2]); // 4
Auto-vivification
function createAutoVivify() {
return new Proxy(
{},
{
get(target, property) {
if (!(property in target)) {
target[property] = createAutoVivify();
}
return target[property];
},
}
);
}
const data = createAutoVivify();
data.a.b.c.d = 'deep value';
console.log(data.a.b.c.d); // "deep value"
Logging Proxy
function createLoggingProxy(target, name = 'Object') {
return new Proxy(target, {
get(target, property, receiver) {
console.log(`[${name}] GET ${String(property)}`);
const value = Reflect.get(target, property, receiver);
if (typeof value === 'object' && value !== null) {
return createLoggingProxy(value, `${name}.${String(property)}`);
}
return value;
},
set(target, property, value, receiver) {
console.log(
`[${name}] SET ${String(property)} = ${JSON.stringify(value)}`
);
return Reflect.set(target, property, value, receiver);
},
});
}
const logged = createLoggingProxy({ user: { name: 'John' } });
logged.user.name = 'Jane';
// [Object] GET user
// [Object.user] SET name = "Jane"
Memoization Proxy
function 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 = Reflect.apply(target, thisArg, args);
cache.set(key, result);
return result;
},
});
}
const expensiveFn = memoize((n) => {
console.log(`Computing for ${n}...`);
return n * 2;
});
expensiveFn(5); // "Computing for 5...", 10
expensiveFn(5); // 10 (cached)
Revocable Proxy
const { proxy, revoke } = Proxy.revocable(
{ name: 'John' },
{
get(target, property) {
return target[property];
},
}
);
console.log(proxy.name); // "John"
revoke();
console.log(proxy.name); // TypeError: Cannot perform 'get' on revoked proxy
Complete Trap Reference
| Trap | Triggered By |
|---|---|
| get | property access |
| set | property assignment |
| has | in operator |
| deleteProperty | delete operator |
| ownKeys | Object.keys(), for...in |
| getOwnPropertyDescriptor | Object.getOwnPropertyDescriptor() |
| defineProperty | Object.defineProperty() |
| getPrototypeOf | Object.getPrototypeOf() |
| setPrototypeOf | Object.setPrototypeOf() |
| isExtensible | Object.isExtensible() |
| preventExtensions | Object.preventExtensions() |
| apply | function call |
| construct | new operator |
Performance Considerations
- •Proxies add overhead to every intercepted operation
- •Use sparingly in performance-critical code
- •Consider alternatives for simple cases
- •Revocable proxies are slightly slower
Browser Support
Proxies are well-supported in modern browsers but cannot be polyfilled due to their fundamental nature.
Summary
| Concept | Purpose |
|---|---|
| Proxy | Intercept object operations |
| Reflect | Consistent API for object operations |
| Traps | Handler methods for specific operations |
| Revocable Proxy | Proxy that can be disabled |