JavaScript Proxies and Reflect
JavaScript Proxies and the Reflect API are powerful features introduced in ES6 (ES2015) that allow developers to intercept and customize operations on objects. These features enable metaprogramming capabilities, making it possible to implement custom behaviors for fundamental operations like property lookup, assignment, enumeration, and function invocation.
Proxies
A Proxy object wraps another object (the target) and intercepts operations like property lookup, assignment, enumeration, and function invocation, optionally modifying their behavior.
Basic Syntax
// Syntax: new Proxy(target, handler)
const target = {
message: 'Hello, World!'
};
const handler = {
get: function(target, prop, receiver) {
console.log(`Getting property: ${prop}`);
return target[prop];
}
};
const proxy = new Proxy(target, handler);
// Using the proxy
console.log(proxy.message);
// Logs: "Getting property: message"
// Then logs: "Hello, World!"
Proxy Handlers (Traps)
Handlers define "traps" that intercept different operations on the target object. Here are the most commonly used traps:
Handler Trap | Description | Intercepted Operations |
---|---|---|
get |
Intercepts property access | proxy.property , proxy['property'] |
set |
Intercepts property assignment | proxy.property = value |
has |
Intercepts the in operator |
'property' in proxy |
deleteProperty |
Intercepts the delete operator |
delete proxy.property |
apply |
Intercepts function calls | proxy(...args) , proxy.call() , proxy.apply() |
construct |
Intercepts new operator |
new proxy(...args) |
getPrototypeOf |
Intercepts getting the prototype | Object.getPrototypeOf(proxy) |
setPrototypeOf |
Intercepts setting the prototype | Object.setPrototypeOf(proxy) |
isExtensible |
Intercepts Object.isExtensible() |
Object.isExtensible(proxy) |
preventExtensions |
Intercepts Object.preventExtensions() |
Object.preventExtensions(proxy) |
getOwnPropertyDescriptor |
Intercepts Object.getOwnPropertyDescriptor() |
Object.getOwnPropertyDescriptor(proxy, 'property') |
defineProperty |
Intercepts Object.defineProperty() |
Object.defineProperty(proxy, 'property', descriptor) |
ownKeys |
Intercepts Object.keys() and related methods |
Object.keys(proxy) , for...in loops |
Common Use Cases for Proxies
1. Validation
// User object with validation
function createUser(name, age) {
const user = {
name,
age
};
return new Proxy(user, {
set(target, prop, value) {
if (prop === 'name' && (typeof value !== 'string' || value.length < 2)) {
throw new Error('Name must be a string with at least 2 characters');
}
if (prop === 'age' && (typeof value !== 'number' || value < 0 || value > 120)) {
throw new Error('Age must be a number between 0 and 120');
}
target[prop] = value;
return true; // Indicate success
}
});
}
const user = createUser('Alice', 30);
user.name = 'Bob'; // Works fine
// user.name = ''; // Error: Name must be a string with at least 2 characters
// user.age = -5; // Error: Age must be a number between 0 and 120
2. Logging and Debugging
// Logging proxy that tracks all property access and modifications
function createLoggingProxy(target, name = 'Object') {
return new Proxy(target, {
get(target, prop, receiver) {
const value = target[prop];
console.log(`${name}.${prop} accessed, returned: ${value}`);
return value;
},
set(target, prop, value, receiver) {
console.log(`${name}.${prop} changed from ${target[prop]} to ${value}`);
target[prop] = value;
return true;
}
});
}
const user = createLoggingProxy({ name: 'Alice', role: 'Admin' }, 'user');
console.log(user.name); // Logs: "user.name accessed, returned: Alice" then "Alice"
user.role = 'User'; // Logs: "user.role changed from Admin to User"
3. Default Values
// Object with default values for missing properties
function withDefaults(target, defaults) {
return new Proxy(target, {
get(target, prop, receiver) {
if (prop in target) {
return target[prop];
}
if (prop in defaults) {
return defaults[prop];
}
return undefined;
}
});
}
const settings = withDefaults({
theme: 'dark'
}, {
theme: 'light',
fontSize: 16,
showSidebar: true
});
console.log(settings.theme); // "dark" (from target)
console.log(settings.fontSize); // 16 (from defaults)
console.log(settings.unknownProp); // undefined
4. Negative Array Indices (Like Python)
// Array with support for negative indices
function createArrayWithNegativeIndices(array) {
return new Proxy(array, {
get(target, prop, receiver) {
// Handle numeric indices
if (typeof prop === 'string' && /^-\d+$/.test(prop)) {
// Convert negative index string to number and calculate positive index
const index = target.length + parseInt(prop, 10);
return target[index];
}
return Reflect.get(target, prop, receiver);
}
});
}
const arr = createArrayWithNegativeIndices([1, 2, 3, 4, 5]);
console.log(arr[1]); // 2
console.log(arr[-1]); // 5 (last element)
console.log(arr[-2]); // 4 (second-to-last element)
5. Hidden Properties
// Object with hidden properties that don't show up in enumeration
function createObjectWithHiddenProps(visible, hidden) {
return new Proxy(visible, {
get(target, prop, receiver) {
// Check visible properties first
if (prop in target) {
return target[prop];
}
// Then check hidden properties
if (prop in hidden) {
return hidden[prop];
}
return undefined;
},
has(target, prop) {
// Only report visible properties with 'in' operator
return prop in target;
},
ownKeys(target) {
// Only enumerate visible properties
return Reflect.ownKeys(target);
},
getOwnPropertyDescriptor(target, prop) {
// Only return descriptors for visible properties
return Reflect.getOwnPropertyDescriptor(target, prop);
}
});
}
const obj = createObjectWithHiddenProps(
{ visible: 'I can be seen' },
{ secret: 'I am hidden' }
);
console.log(obj.visible); // "I can be seen"
console.log(obj.secret); // "I am hidden"
console.log('secret' in obj); // false
console.log(Object.keys(obj)); // ["visible"]
Note: Proxies are not transparent for identity operations. The original target and the proxy are different objects:
const target = {};
const proxy = new Proxy(target, {});
console.log(target === proxy); // false
Reflect API
The Reflect API provides methods that are the same as the proxy handler methods. It's a built-in object that provides methods for interceptable JavaScript operations, making it easier to implement proxies.
Basic Usage
const obj = { x: 1, y: 2 };
// Traditional way to access properties
console.log(obj.x); // 1
// Using Reflect
console.log(Reflect.get(obj, 'x')); // 1
// Traditional way to set properties
obj.y = 3;
// Using Reflect
Reflect.set(obj, 'y', 4);
console.log(obj.y); // 4
Why Use Reflect?
- Consistent Function Interface: All operations are methods, not operators.
- Status Returns: Many methods return boolean success status instead of throwing errors.
- Receiver Forwarding: Makes it easier to forward operations to the original target in Proxy handlers.
- More Reliable: Some operations that can fail silently with traditional syntax will return a status with Reflect.
Common Reflect Methods
Reflect Method | Equivalent Traditional Syntax | Description |
---|---|---|
Reflect.get(target, prop, receiver?) |
target[prop] |
Gets a property value |
Reflect.set(target, prop, value, receiver?) |
target[prop] = value |
Sets a property value |
Reflect.has(target, prop) |
prop in target |
Checks if property exists |
Reflect.deleteProperty(target, prop) |
delete target[prop] |
Deletes a property |
Reflect.apply(target, thisArg, args) |
target.apply(thisArg, args) |
Calls a function |
Reflect.construct(target, args, newTarget?) |
new target(...args) |
Creates a new instance |
Reflect.defineProperty(target, prop, descriptor) |
Object.defineProperty(target, prop, descriptor) |
Defines a property |
Reflect.getOwnPropertyDescriptor(target, prop) |
Object.getOwnPropertyDescriptor(target, prop) |
Gets property descriptor |
Reflect.getPrototypeOf(target) |
Object.getPrototypeOf(target) |
Gets prototype |
Reflect.setPrototypeOf(target, prototype) |
Object.setPrototypeOf(target, prototype) |
Sets prototype |
Reflect.isExtensible(target) |
Object.isExtensible(target) |
Checks if object is extensible |
Reflect.preventExtensions(target) |
Object.preventExtensions(target) |
Prevents extensions |
Reflect.ownKeys(target) |
Object.getOwnPropertyNames(target).concat(Object.getOwnPropertySymbols(target)) |
Gets all own keys |
Using Reflect with Proxies
Reflect methods are particularly useful in Proxy handlers to forward operations to the target object:
const target = {
name: 'Target',
greet() {
return `Hello, my name is ${this.name}`;
}
};
const handler = {
get(target, prop, receiver) {
console.log(`Getting ${prop}`);
// Forward the operation to the target, preserving 'this' binding
return Reflect.get(target, prop, receiver);
}
};
const proxy = new Proxy(target, handler);
// The 'this' in target.greet() will correctly refer to the proxy
console.log(proxy.greet()); // "Getting greet" then "Hello, my name is Target"
Important: Without using Reflect.get
with the receiver parameter, the this
value inside methods would refer to the target, not the proxy, which can cause unexpected behavior.
Advanced Patterns with Proxies and Reflect
1. Method Chaining with Proxies
// Create a chainable API with proxies
function createChainableAPI(baseObject) {
return new Proxy(baseObject, {
get(target, prop, receiver) {
// Get the actual property
const value = Reflect.get(target, prop, receiver);
// If it's a method, wrap it to return the proxy
if (typeof value === 'function') {
return function(...args) {
const result = value.apply(this, args);
// If the method returns 'this', return the proxy instead
return result === target ? receiver : result;
};
}
return value;
}
});
}
// Example: Chainable calculator
const calculator = createChainableAPI({
value: 0,
add(n) {
this.value += n;
return this;
},
subtract(n) {
this.value -= n;
return this;
},
multiply(n) {
this.value *= n;
return this;
},
divide(n) {
this.value /= n;
return this;
},
result() {
return this.value;
}
});
// Chain methods
const result = calculator
.add(5)
.multiply(2)
.subtract(3)
.divide(2)
.result();
console.log(result); // ((5 * 2) - 3) / 2 = 3.5
2. Revocable Proxies
JavaScript provides a way to create proxies that can be disabled:
// Create a revocable proxy
const target = { message: 'Hello' };
const { proxy, revoke } = Proxy.revocable(target, {
get(target, prop, receiver) {
console.log(`Accessing ${prop}`);
return Reflect.get(target, prop, receiver);
}
});
// Use the proxy
console.log(proxy.message); // "Accessing message" then "Hello"
// Revoke access - the proxy becomes unusable
revoke();
// Any further attempt to use the proxy will throw a TypeError
// console.log(proxy.message); // TypeError: Cannot perform 'get' on a proxy that has been revoked
Use Case: Revocable proxies are useful for controlling access to objects, especially when you need to grant temporary access that can be revoked later, such as in API tokens or temporary permissions.
3. Virtual Properties
// Object with computed properties that don't actually exist
function createObjectWithVirtualProps(realData) {
return new Proxy(realData, {
get(target, prop, receiver) {
// Handle special virtual properties
if (prop === 'fullName') {
return `${target.firstName} ${target.lastName}`;
}
if (prop === 'age') {
const birthYear = new Date(target.birthDate).getFullYear();
const currentYear = new Date().getFullYear();
return currentYear - birthYear;
}
// Default behavior for real properties
return Reflect.get(target, prop, receiver);
}
});
}
const person = createObjectWithVirtualProps({
firstName: 'John',
lastName: 'Doe',
birthDate: '1990-05-15'
});
console.log(person.firstName); // "John" (real property)
console.log(person.fullName); // "John Doe" (virtual property)
console.log(person.age); // Current year - 1990 (virtual property)
Performance Considerations
While Proxies and Reflect provide powerful capabilities, they do come with some performance overhead:
- Proxies add a layer of indirection to every operation, which can impact performance in performance-critical code.
- Property access through Proxies is slower than direct property access on regular objects.
- Creating many Proxy objects can increase memory usage.
Best Practice: Use Proxies for specific use cases where their benefits outweigh the performance cost. For high-performance code paths that are called frequently, consider alternative approaches.
Browser Support
Proxies and Reflect are supported in all modern browsers, but they are not available in Internet Explorer. They were introduced in ES6 (ES2015) and have good support in:
- Chrome 49+
- Firefox 18+
- Safari 10+
- Edge 12+
- Node.js 6.0.0+
Note: Proxies cannot be polyfilled or transpiled for older browsers due to their fundamental nature. If you need to support older browsers, you'll need to use alternative approaches.
Summary
JavaScript Proxies and the Reflect API provide powerful metaprogramming capabilities that allow you to:
- Intercept and customize fundamental object operations
- Implement validation, logging, default values, and other cross-cutting concerns
- Create virtual objects and properties
- Build more maintainable and flexible APIs
While they come with some performance overhead, they offer elegant solutions to many complex programming challenges that would otherwise require more verbose and less maintainable code.