JavaScript Proxy and Reflect API: Metaprogramming Power
Master JavaScript's Proxy and Reflect APIs for powerful metaprogramming. Learn to intercept operations, create reactive objects, and implement advanced patterns.
The Proxy and Reflect APIs, introduced in ES6, provide powerful metaprogramming capabilities in JavaScript. They allow you to intercept and customize fundamental operations on objects, opening up new possibilities for reactive programming, validation, and more.
Understanding Proxy
A Proxy wraps another object (the target) and intercepts operations performed on it. You define handler functions (traps) that run when specific operations occur.
Basic Proxy Creation
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} = ${value}`);
target[property] = value;
return true;
},
};
const proxy = new Proxy(target, handler);
// Using the proxy
console.log(proxy.name); // Logs: Getting name, John
proxy.age = 31; // Logs: Setting age = 31
Common Proxy Traps
Property Access Traps
const user = {
name: 'Alice',
_id: '123',
email: 'alice@example.com',
};
const secureUser = new Proxy(user, {
// get trap
get(target, prop) {
if (prop.startsWith('_')) {
throw new Error(`Access denied to private property '${prop}'`);
}
return target[prop];
},
// set trap
set(target, prop, value) {
if (prop.startsWith('_')) {
throw new Error(`Cannot set private property '${prop}'`);
}
if (prop === 'email' && !value.includes('@')) {
throw new Error('Invalid email format');
}
target[prop] = value;
return true;
},
// has trap (for 'in' operator)
has(target, prop) {
if (prop.startsWith('_')) {
return false;
}
return prop in target;
},
// deleteProperty trap
deleteProperty(target, prop) {
if (prop.startsWith('_')) {
throw new Error(`Cannot delete private property '${prop}'`);
}
delete target[prop];
return true;
},
});
console.log(secureUser.name); // Alice
console.log('_id' in secureUser); // false
// secureUser._id // Error: Access denied
Function Traps
// apply trap for function calls
function sum(...args) {
return args.reduce((a, b) => a + b, 0);
}
const trackedSum = new Proxy(sum, {
apply(target, thisArg, argumentsList) {
console.log(`Called with args: ${argumentsList}`);
const result = target.apply(thisArg, argumentsList);
console.log(`Result: ${result}`);
return result;
},
});
trackedSum(1, 2, 3); // Logs call info, returns 6
// construct trap for 'new' operator
class User {
constructor(name) {
this.name = name;
}
}
const TrackedUser = new Proxy(User, {
construct(target, args, newTarget) {
console.log(`Creating user: ${args[0]}`);
return new target(...args);
},
});
const user = new TrackedUser('Bob'); // Logs: Creating user: Bob
Real-World Proxy Patterns
1. Reactive Objects (Like Vue.js)
function createReactive(target, onChange) {
const handlers = {
get(target, property, receiver) {
const value = Reflect.get(target, property, receiver);
// Make nested objects reactive too
if (value && typeof value === 'object') {
return createReactive(value, onChange);
}
return value;
},
set(target, property, value, receiver) {
const oldValue = target[property];
const result = Reflect.set(target, property, value, receiver);
if (oldValue !== value) {
onChange(property, value, oldValue);
}
return result;
},
deleteProperty(target, property) {
const oldValue = target[property];
const result = Reflect.deleteProperty(target, property);
if (result) {
onChange(property, undefined, oldValue);
}
return result;
},
};
return new Proxy(target, handlers);
}
// Usage
const state = createReactive(
{
user: {
name: 'Alice',
settings: {
theme: 'dark',
},
},
count: 0,
},
(prop, newVal, oldVal) => {
console.log(`${prop} changed from ${oldVal} to ${newVal}`);
}
);
state.count = 1; // count changed from 0 to 1
state.user.name = 'Bob'; // name changed from Alice to Bob
state.user.settings.theme = 'light'; // theme changed from dark to light
2. Validation Proxy
function createValidator(target, schema) {
return new Proxy(target, {
set(target, property, value) {
const validator = schema[property];
if (!validator) {
throw new Error(`Unknown property: ${property}`);
}
if (validator.type && typeof value !== validator.type) {
throw new TypeError(`${property} must be ${validator.type}`);
}
if (validator.validator && !validator.validator(value)) {
throw new Error(`Invalid value for ${property}`);
}
if (validator.transform) {
value = validator.transform(value);
}
return Reflect.set(target, property, value);
},
});
}
// Schema definition
const userSchema = {
name: {
type: 'string',
validator: (v) => v.length > 0,
},
age: {
type: 'number',
validator: (v) => v >= 0 && v <= 150,
},
email: {
type: 'string',
validator: (v) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v),
transform: (v) => v.toLowerCase(),
},
};
const user = createValidator({}, userSchema);
user.name = 'John'; // OK
user.email = 'JOHN@EXAMPLE.COM'; // Stored as john@example.com
// user.age = '30'; // TypeError: age must be number
// user.age = 200; // Error: Invalid value for age
3. Property Case Conversion
function caseInsensitiveProxy(target) {
return new Proxy(target, {
get(target, property) {
if (typeof property === 'string') {
property = property.toLowerCase();
}
return Reflect.get(target, property);
},
set(target, property, value) {
if (typeof property === 'string') {
property = property.toLowerCase();
}
return Reflect.set(target, property, value);
},
has(target, property) {
if (typeof property === 'string') {
property = property.toLowerCase();
}
return Reflect.has(target, property);
},
});
}
const headers = caseInsensitiveProxy({});
headers['Content-Type'] = 'application/json';
console.log(headers['content-type']); // application/json
console.log(headers['CONTENT-TYPE']); // application/json
4. Array Negative Indexing
function createArrayWithNegativeIndex(arr) {
return new Proxy(arr, {
get(target, property) {
if (!isNaN(property)) {
const index = Number(property);
if (index < 0) {
return target[target.length + index];
}
}
return Reflect.get(target, property);
},
set(target, property, value) {
if (!isNaN(property)) {
const index = Number(property);
if (index < 0) {
target[target.length + index] = value;
return true;
}
}
return Reflect.set(target, property, value);
},
});
}
const arr = createArrayWithNegativeIndex([1, 2, 3, 4, 5]);
console.log(arr[-1]); // 5
console.log(arr[-2]); // 4
arr[-1] = 10;
console.log(arr); // [1, 2, 3, 4, 10]
The Reflect API
Reflect provides methods that mirror proxy traps, offering a cleaner way to perform object operations.
Why Use Reflect?
const obj = { a: 1 };
// Old way
try {
Object.defineProperty(obj, 'b', {
value: 2,
writable: false,
});
} catch (e) {
console.error(e);
}
// New way with Reflect
const success = Reflect.defineProperty(obj, 'b', {
value: 2,
writable: false,
});
if (!success) {
console.error('Failed to define property');
}
// Reflect methods return booleans instead of throwing
Common Reflect Methods
const obj = { name: 'John', age: 30 };
// Reflect.get
console.log(Reflect.get(obj, 'name')); // John
// Reflect.set
Reflect.set(obj, 'city', 'New York');
console.log(obj.city); // New York
// Reflect.has
console.log(Reflect.has(obj, 'age')); // true
// Reflect.deleteProperty
Reflect.deleteProperty(obj, 'age');
console.log(obj.age); // undefined
// Reflect.ownKeys
console.log(Reflect.ownKeys(obj)); // ['name', 'city']
// Reflect.construct
class Person {
constructor(name) {
this.name = name;
}
}
const person = Reflect.construct(Person, ['Alice']);
console.log(person.name); // Alice
// Reflect.apply
function greet(greeting) {
return `${greeting}, ${this.name}!`;
}
const user = { name: 'Bob' };
console.log(Reflect.apply(greet, user, ['Hello'])); // Hello, Bob!
Advanced Patterns
Revocable Proxy
const target = {
data: 'sensitive',
};
const { proxy, revoke } = Proxy.revocable(target, {
get(target, property) {
console.log(`Accessing ${property}`);
return target[property];
},
});
console.log(proxy.data); // Accessing data, sensitive
// Revoke access
revoke();
// Any operation on proxy now throws
try {
console.log(proxy.data);
} catch (e) {
console.error('Proxy revoked'); // This executes
}
Membrane Pattern
function createMembrane(target) {
const proxyCache = new WeakMap();
let revoked = false;
function wrap(obj) {
if (revoked) {
throw new Error('Membrane revoked');
}
if (obj === null || typeof obj !== 'object') {
return obj;
}
if (proxyCache.has(obj)) {
return proxyCache.get(obj);
}
const proxy = new Proxy(obj, {
get(target, property) {
if (revoked) throw new Error('Membrane revoked');
return wrap(Reflect.get(target, property));
},
set(target, property, value) {
if (revoked) throw new Error('Membrane revoked');
return Reflect.set(target, property, unwrap(value));
},
});
proxyCache.set(obj, proxy);
return proxy;
}
function unwrap(obj) {
if (proxyCache.has(obj)) {
return (
[...proxyCache.entries()].find(([_, proxy]) => proxy === obj)?.[0] ||
obj
);
}
return obj;
}
return {
proxy: wrap(target),
revoke() {
revoked = true;
},
};
}
// Usage
const obj = {
user: {
name: 'Alice',
permissions: ['read', 'write'],
},
};
const { proxy, revoke } = createMembrane(obj);
console.log(proxy.user.name); // Alice
revoke();
// Now all access is revoked
// proxy.user.name // Error: Membrane revoked
Observable Pattern
class Observable {
constructor(target) {
this.observers = new Map();
return new Proxy(target, {
set: (target, property, value) => {
const result = Reflect.set(target, property, value);
this.notify(property, value);
return result;
},
});
}
subscribe(property, callback) {
if (!this.observers.has(property)) {
this.observers.set(property, new Set());
}
this.observers.get(property).add(callback);
// Return unsubscribe function
return () => {
const callbacks = this.observers.get(property);
if (callbacks) {
callbacks.delete(callback);
}
};
}
notify(property, value) {
const callbacks = this.observers.get(property);
if (callbacks) {
callbacks.forEach((callback) => callback(value));
}
}
}
// Usage
const state = new Observable({
count: 0,
user: null,
});
const unsubscribe1 = state.subscribe('count', (value) => {
console.log(`Count changed to: ${value}`);
});
const unsubscribe2 = state.subscribe('user', (value) => {
console.log(`User changed to:`, value);
});
state.count = 1; // Count changed to: 1
state.count = 2; // Count changed to: 2
state.user = { name: 'Bob' }; // User changed to: { name: 'Bob' }
unsubscribe1();
state.count = 3; // No log (unsubscribed)
Performance Considerations
// Performance test
const iterations = 1000000;
// Direct access
const directObj = { value: 0 };
console.time('Direct');
for (let i = 0; i < iterations; i++) {
directObj.value = i;
}
console.timeEnd('Direct');
// Proxy access
const proxyObj = new Proxy(
{ value: 0 },
{
set(target, prop, value) {
target[prop] = value;
return true;
},
}
);
console.time('Proxy');
for (let i = 0; i < iterations; i++) {
proxyObj.value = i;
}
console.timeEnd('Proxy');
// Proxies are slower but the overhead is usually acceptable
// for the functionality they provide
Best Practices
- Use Reflect in trap handlers for consistency and proper behavior
- Return appropriate values from traps (especially boolean traps)
- Consider performance when using proxies in hot code paths
- Document proxy behavior as it can make code harder to understand
- Use revocable proxies for temporary access control
- Cache nested proxies to avoid creating multiple proxies for the same object
Conclusion
Proxy and Reflect APIs are powerful tools for metaprogramming in JavaScript. They enable patterns like reactive programming, validation, access control, and more. While they add some complexity and performance overhead, they provide elegant solutions to problems that would otherwise require much more code or be impossible to implement.