Introduction
Proxy and Reflect are powerful ES6 features that enable metaprogramming in JavaScript. Proxy allows you to intercept and customize fundamental operations on objects, while Reflect provides methods for interceptable object operations.
This guide explores Proxy traps, Reflect API, and practical patterns for validation, logging, reactive data, and access control.
Understanding Proxy and Reflect: Core Concepts
What is a Proxy?
A Proxy wraps an object and intercepts operations like property access, assignment, and function calls:
const target = { name: 'John', age: 30 };
const handler = {
get(target, property, receiver) {
console.log(`Getting ${property}`);
return Reflect.get(target, property, receiver);
},
set(target, property, value, receiver) {
console.log(`Setting ${property} to ${value}`);
return Reflect.set(target, property, value, receiver);
},
};
const proxy = new Proxy(target, handler);
proxy.name; // Logs: Getting name
proxy.age = 31; // Logs: Setting age to 31Proxy Traps
Proxy supports 13 traps for different operations:
const handler = {
// Property access
get(target, prop, receiver) { },
// Property assignment
set(target, prop, value, receiver) { },
// Property deletion
deleteProperty(target, prop) { },
// Property existence check
has(target, prop) { },
// Function call
apply(target, thisArg, args) { },
// Constructor call
construct(target, args, newTarget) { },
// Object.defineProperty
defineProperty(target, prop, descriptor) { },
// Object.getOwnPropertyDescriptor
getOwnPropertyDescriptor(target, prop) { },
// Object.getPrototypeOf
getPrototypeOf(target) { },
// Object.setPrototypeOf
setPrototypeOf(target, prototype) { },
// Object.keys, for...in
ownKeys(target) { },
// Object.preventExtensions
preventExtensions(target) { },
// Object.isExtensible
isExtensible(target) { },
};Reflect API
Reflect provides methods corresponding to Proxy traps:
const obj = { a: 1, b: 2 };
// Without Reflect
handler.get = function(target, prop) {
return prop in target ? target[prop] : undefined;
};
// With Reflect
handler.get = function(target, prop, receiver) {
return Reflect.get(target, prop, receiver);
};
// Reflect methods
Reflect.get(obj, 'a'); // 1
Reflect.set(obj, 'c', 3); // true
Reflect.has(obj, 'a'); // true
Reflect.deleteProperty(obj, 'b'); // true
Reflect.ownKeys(obj); // ['a', 'c']Architecture and Design Patterns
Validation Proxy
function createValidated(schema) {
return new Proxy({}, {
set(target, prop, value) {
if (schema[prop]) {
const { type, required, validate } = schema[prop];
if (required && (value === undefined || value === null)) {
throw new Error(`${prop} is required`);
}
if (type && typeof value !== type) {
throw new Error(`${prop} must be of type ${type}`);
}
if (validate && !validate(value)) {
throw new Error(`Validation failed for ${prop}`);
}
}
return Reflect.set(target, prop, value);
},
});
}
// Usage
const userSchema = {
name: { type: 'string', required: true },
age: { type: 'number', validate: (v) => v >= 0 && v <= 150 },
email: { type: 'string', validate: (v) => v.includes('@') },
};
const user = createValidated(userSchema);
user.name = 'John'; // Valid
user.age = 30; // Valid
user.age = -5; // Error: Validation failed for ageReactive Objects
function createReactive(target, onChange) {
const handler = {
get(target, prop, receiver) {
const value = Reflect.get(target, prop, receiver);
// Deep reactivity for nested objects
if (typeof value === 'object' && value !== null) {
return createReactive(value, (nestedProp, nestedValue) => {
onChange(`${prop}.${nestedProp}`, nestedValue);
});
}
return value;
},
set(target, prop, value, receiver) {
const oldValue = target[prop];
const result = Reflect.set(target, prop, value, receiver);
if (oldValue !== value) {
onChange(prop, value);
}
return result;
},
deleteProperty(target, prop) {
const result = Reflect.deleteProperty(target, prop);
onChange(prop, undefined);
return result;
},
};
return new Proxy(target, handler);
}
// Usage
const state = createReactive({ count: 0, user: { name: 'John' } }, (prop, value) => {
console.log(`Changed: ${prop} = ${JSON.stringify(value)}`);
});
state.count = 1; // Logs: Changed: count = 1
state.user.name = 'Jane'; // Logs: Changed: user.name = "Jane"Access Control Proxy
function createAccessControl(target, permissions) {
return new Proxy(target, {
get(target, prop, receiver) {
if (prop in permissions && !permissions[prop].read) {
throw new Error(`Access denied: cannot read ${prop}`);
}
const value = Reflect.get(target, prop, receiver);
// Return method bound to proxy for correct 'this' context
if (typeof value === 'function') {
return value.bind(receiver);
}
return value;
},
set(target, prop, value, receiver) {
if (prop in permissions && !permissions[prop].write) {
throw new Error(`Access denied: cannot write ${prop}`);
}
return Reflect.set(target, prop, value, receiver);
},
deleteProperty(target, prop) {
if (prop in permissions && !permissions[prop].delete) {
throw new Error(`Access denied: cannot delete ${prop}`);
}
return Reflect.deleteProperty(target, prop);
},
});
}
// Usage
const data = { public: 'visible', secret: 'hidden', config: 'settings' };
const permissions = {
public: { read: true, write: true, delete: false },
secret: { read: false, write: false, delete: false },
config: { read: true, write: false, delete: false },
};
const protectedData = createAccessControl(data, permissions);
console.log(protectedData.public); // 'visible'
console.log(protectedData.secret); // Error: Access deniedStep-by-Step Implementation
Logging Proxy
function createLogger(target, name = 'Object') {
return new Proxy(target, {
get(target, prop, receiver) {
const value = Reflect.get(target, prop, receiver);
if (typeof value === 'function') {
return function (...args) {
console.log(`[${name}] Calling ${prop}(${args.map(a => JSON.stringify(a)).join(', ')})`);
const result = value.apply(this, args);
console.log(`[${name}] ${prop} returned ${JSON.stringify(result)}`);
return result;
};
}
console.log(`[${name}] Getting ${prop} = ${JSON.stringify(value)}`);
return value;
},
set(target, prop, value, receiver) {
console.log(`[${name}] Setting ${prop} = ${JSON.stringify(value)}`);
return Reflect.set(target, prop, value, receiver);
},
});
}
// Usage
const api = createLogger({
fetchUser(id) {
return { id, name: 'John' };
},
calculate(a, b) {
return a + b;
},
}, 'API');
api.fetchUser(1);
api.calculate(2, 3);Type Coercion Proxy
function createCoercing(target) {
return new Proxy(target, {
get(target, prop, receiver) {
const value = Reflect.get(target, prop, receiver);
// Return default values for undefined properties
if (value === undefined) {
return 0; // or '' for strings, false for booleans
}
return value;
},
set(target, prop, value, receiver) {
// Coerce types
const currentValue = target[prop];
if (typeof currentValue === 'number') {
value = Number(value) || 0;
} else if (typeof currentValue === 'string') {
value = String(value);
} else if (typeof currentValue === 'boolean') {
value = Boolean(value);
}
return Reflect.set(target, prop, value, receiver);
},
});
}
// Usage
const stats = createCoercing({ hits: 0, misses: 0, rate: 0.0 });
stats.hits = '42'; // Coerced to 42
stats.misses = null; // Coerced to 0
console.log(stats.nonexistent); // Returns 0 instead of undefinedReal-World Use Cases
Vue.js Reactivity
Vue 3 uses Proxy for its reactivity system, replacing Object.defineProperty from Vue 2.
State Management Libraries
Libraries like MobX and Immer use Proxy to track changes and implement immutable updates.
API Client Wrapping
function createApiClient(baseUrl) {
return new Proxy({}, {
get(target, resource) {
return new Proxy(() => {}, {
apply(target, thisArg, [id]) {
return fetch(`${baseUrl}/${resource}/${id}`).then(r => r.json());
},
get(target, action) {
return (data) => {
const method = action === 'create' ? 'POST' :
action === 'update' ? 'PUT' :
action === 'delete' ? 'DELETE' : 'GET';
return fetch(`${baseUrl}/${resource}`, {
method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
}).then(r => r.json());
};
},
});
},
});
}
// Usage
const api = createApiClient('https://api.example.com');
await api.users(123); // GET /users/123
await api.users.create({ name: 'John' }); // POST /usersBest Practices for Production
-
Use Reflect for default behavior: Always delegate to Reflect in traps.
-
Keep traps minimal: Avoid heavy computation in traps.
-
Document proxy behavior: Proxies can be surprising; document their behavior.
-
Test proxy interactions: Ensure proxies work with existing code.
-
Consider performance: Proxies add overhead; use judiciously.
-
Handle nested objects: Decide on deep vs shallow proxying.
Common Pitfalls and Solutions
| Pitfall | Impact | Solution |
|---|---|---|
| Forgetting Reflect delegation | Broken default behavior | Always use Reflect in traps |
| Proxy identity issues | Comparison failures | Store original reference if needed |
| Performance overhead | Slower code | Use proxies only when necessary |
| Deep proxy complexity | Memory overhead | Proxy only top level if possible |
// Pitfall: Broken 'this' context
const target = {
name: 'John',
greet() {
return `Hello, ${this.name}`;
},
};
const proxy = new Proxy(target, {
get(target, prop, receiver) {
// Wrong: return target[prop];
// Correct: return Reflect.get(target, prop, receiver);
return Reflect.get(target, prop, receiver);
},
});
proxy.greet(); // Correctly returns "Hello, John"Performance Optimization
// Avoid creating proxies in hot paths
function createCachedProxy(factory) {
const cache = new WeakMap();
return function getProxy(target) {
if (!cache.has(target)) {
cache.set(target, factory(target));
}
return cache.get(target);
};
}
const getReactive = createCachedProxy((target) => {
return new Proxy(target, { /* handlers */ });
});Comparison with Alternatives
| Feature | Proxy | Object.defineProperty | Symbols |
|---|---|---|---|
| Intercept reads | Yes | Yes | No |
| Intercept writes | Yes | Yes | No |
| New properties | Automatic | Manual | N/A |
| Array methods | Yes | No | N/A |
| Performance | Good | Excellent | Excellent |
Testing Strategies
describe('Proxy', () => {
it('should intercept property access', () => {
const handler = {
get: jest.fn((target, prop) => Reflect.get(target, prop)),
};
const proxy = new Proxy({ a: 1 }, handler);
const value = proxy.a;
expect(value).toBe(1);
expect(handler.get).toHaveBeenCalled();
});
it('should intercept property assignment', () => {
const handler = {
set: jest.fn((target, prop, value) => Reflect.set(target, prop, value)),
};
const proxy = new Proxy({}, handler);
proxy.a = 1;
expect(handler.set).toHaveBeenCalled();
expect(proxy.a).toBe(1);
});
});Future Outlook
Proxy and Reflect are stable and widely supported. The TC39 pipeline operator and decorators may provide alternative metaprogramming patterns, but Proxy remains essential for advanced object interception.
Performance Implications
Proxies introduce overhead because every property access, method call, and assignment goes through the proxy trap function. In performance-critical code paths, this overhead can be significant. Benchmarks show that proxied property access is five to ten times slower than direct property access, depending on the trap complexity. For this reason, use proxies judiciously in hot code paths. Consider using proxies during development for debugging and validation, and removing them in production builds. The Reflect API has negligible performance overhead since it directly calls the internal methods. Use Reflect operations inside proxy traps to maintain correct behavior without additional overhead. When performance is critical, consider alternative patterns like getters and setters or explicit method wrapping that achieve similar functionality without the proxy overhead.
Security Considerations
Proxies can be used for security purposes, such as creating sandboxed environments that restrict access to certain objects or APIs. However, proxies are not a security boundary on their own because the proxy target remains accessible through the proxy's internal slots. For true security isolation, use Web Workers or iframes combined with the structured clone algorithm for communication. Proxies are better suited for development-time validation, logging, and access control patterns where the goal is to catch mistakes rather than prevent malicious behavior. Always combine proxies with other security measures when building access control systems.
Validation and Debugging Proxies
One of the most practical applications of Proxy is adding validation and debugging capabilities to objects without modifying their implementation. Create a validation proxy that checks property types, enforces required fields, and prevents access to undefined properties during development. Wrap API response objects in a debugging proxy that logs all property accesses, helping you identify which properties your code actually uses versus which ones you fetch but never read. These patterns are invaluable during development and can be stripped from production builds for zero runtime overhead.
Performance Implications
Proxy objects have performance overhead because every property access, assignment, and method call goes through the trap function. For hot code paths where millions of operations happen per second, the overhead of Proxy can be measurable. In benchmarks, Proxy property access is approximately two to five times slower than direct property access depending on the engine. Avoid using Proxy in performance-critical loops or data processing pipelines. Instead, use Proxy during development for debugging and validation, then remove it in production builds. For runtime metaprogramming in production, prefer simpler patterns like getter and setter functions or wrapper objects.
Vue.js Reactivity: A Deep Dive
Vue 3's reactivity system is built entirely on Proxy. Understanding how Vue uses Proxy illuminates the power and practicality of this feature. When you call reactive() on an object, Vue wraps it in a Proxy that tracks which properties are accessed during component rendering:
// Simplified Vue 3 reactivity implementation
function reactive(target) {
return new Proxy(target, {
get(target, key, receiver) {
track(target, key); // Record dependency
const result = Reflect.get(target, key, receiver);
if (typeof result === 'object' && result !== null) {
return reactive(result); // Deep reactivity
}
return result;
},
set(target, key, value, receiver) {
const oldValue = target[key];
const result = Reflect.set(target, key, value, receiver);
if (oldValue !== value) {
trigger(target, key); // Notify subscribers
}
return result;
},
});
}The key insight is that Vue tracks dependencies at the property level. When a component renders and accesses state.user.name, only the user and name properties are tracked. If state.count changes later, the component does not re-render because it never accessed count. This fine-grained reactivity is only possible with Proxy because it can intercept property access on any object, including properties that did not exist when the Proxy was created.
Vue 2 used Object.defineProperty instead, which had fundamental limitations: it could not detect new property additions, array index changes, or property deletions. Proxy solves all of these problems, which is why Vue 3 made the switch despite the breaking change in behavior.
Advanced Proxy Patterns
Revocable Proxy
A revocable proxy can be disabled after creation, which is useful for implementing access expiration:
function createTemporaryAccess(target, durationMs) {
const { proxy, revoke } = Proxy.revocable(target, {
get(target, prop, receiver) {
return Reflect.get(target, prop, receiver);
},
set(target, prop, value, receiver) {
return Reflect.set(target, prop, value, receiver);
},
});
setTimeout(revoke, durationMs);
return proxy;
}
// Usage: grant access for 5 minutes
const sensitiveData = { secrets: ['a', 'b', 'c'] };
const temporaryAccess = createTemporaryAccess(sensitiveData, 5 * 60 * 1000);
temporaryAccess.secrets; // Works: ['a', 'b', 'c']
// After 5 minutes:
temporaryAccess.secrets; // TypeError: Cannot perform 'get' on a proxy that has been revokedMethod Call Interceptor
function createMethodLogger(target, methodName) {
return new Proxy(target, {
get(target, prop, receiver) {
const value = Reflect.get(target, prop, receiver);
if (prop === methodName && typeof value === 'function') {
return function (...args) {
console.log(`Calling ${methodName} with args:`, args);
const start = performance.now();
const result = value.apply(this, args);
const duration = performance.now() - start;
console.log(`${methodName} returned in ${duration.toFixed(2)}ms:`, result);
return result;
};
}
return value;
},
});
}
// Usage
const service = createMethodLogger(databaseService, 'query');
await service.query('SELECT * FROM users'); // Logs timing and argsImmutable Object Enforcement
function deepFreeze(obj) {
return new Proxy(obj, {
set() {
throw new Error('Cannot modify frozen object');
},
deleteProperty() {
throw new Error('Cannot delete from frozen object');
},
get(target, prop, receiver) {
const value = Reflect.get(target, prop, receiver);
if (typeof value === 'object' && value !== null) {
return deepFreeze(value); // Recursively freeze
}
return value;
},
});
}
const config = deepFreeze({ db: { host: 'localhost', port: 5432 } });
config.db.host = 'remote'; // Error: Cannot modify frozen objectProxy in Production Frameworks
Beyond Vue, several production frameworks rely on Proxy for their core functionality. MobX uses Proxy to automatically track observable state dependencies. Immer uses Proxy to create draft states that record mutations, enabling immutable updates with mutable syntax. Ember.js uses Proxy for its tracked properties system. Each of these frameworks demonstrates that Proxy is not just an academic curiosity but a production-proven tool for building sophisticated developer-facing APIs.
The common pattern across all these frameworks is the same: intercept property access to record dependencies, intercept property mutations to trigger updates, and provide a transparent developer experience where the metaprogramming is invisible to the application developer.
Conclusion
Proxy and Reflect unlock powerful metaprogramming capabilities in JavaScript. Use them for validation, logging, reactivity, and access control patterns that aren't possible with traditional approaches.
Key takeaways:
- Proxy intercepts 13 fundamental object operations
- Reflect provides default implementations for each trap
- Use Proxy for validation, reactivity, and access control
- Always delegate to Reflect for correct default behavior
- Consider performance implications; use judiciously
- Revocable proxies enable time-limited access patterns
- Vue 3's reactivity system demonstrates Proxy's production value
Explore Proxy in the MDN documentation and experiment with patterns on JavaScript.info.