Advanced Features
Proxy & Reflect
Meta-programming with Proxy traps and Reflect API — reactive systems, validation, access control, and more
Proxy & Reflect
Proxy allows you to intercept and customize fundamental operations on objects (property access, assignment, function calls, etc.). Reflect provides methods that mirror these operations with consistent behavior.
Problems Solved
| Problem | How Proxy Solves It |
|---|---|
| No way to observe property changes | set trap detects mutations |
| Manual validation on every assignment | Proxy validates automatically |
| No access control on objects | get/set traps enforce permissions |
| Hard to build reactive UI systems | Proxy enables fine-grained dependency tracking |
| Verbose logging/debugging of object access | Traps log operations transparently |
Cannot intercept delete, in, iteration | Proxy traps cover all fundamental operations |
How Proxy Works
┌─────────────┐
Code reads │ │
obj.name ───► │ Proxy │ ───► get trap ───► return value
│ (Handler) │
Code writes │ │
obj.name = x ──► │ │ ───► set trap ───► validate & assign
└─────────────┘
│
▼
┌─────────────┐
│ Target │ (Original object)
│ Object │
└─────────────┘Basic Usage
const target = { name: 'Alice', age: 30 };
const handler = {
get(target, prop, receiver) {
console.log(`Reading: ${prop}`);
return Reflect.get(target, prop, receiver);
},
set(target, prop, value, receiver) {
console.log(`Writing: ${prop} = ${value}`);
return Reflect.set(target, prop, value, receiver);
}
};
const proxy = new Proxy(target, handler);
proxy.name; // logs "Reading: name" → "Alice"
proxy.age = 31; // logs "Writing: age = 31"Proxy Traps Reference
| Trap | Intercepts | Example |
|---|---|---|
get(target, prop, receiver) | Property read | obj.x, obj['x'] |
set(target, prop, value, receiver) | Property write | obj.x = 1 |
has(target, prop) | in operator | 'x' in obj |
deleteProperty(target, prop) | delete operator | delete obj.x |
ownKeys(target) | Key enumeration | Object.keys(obj) |
getOwnPropertyDescriptor(target, prop) | Descriptor access | Object.getOwnPropertyDescriptor(obj, 'x') |
defineProperty(target, prop, descriptor) | Property definition | Object.defineProperty(obj, 'x', {...}) |
getPrototypeOf(target) | Prototype access | Object.getPrototypeOf(obj) |
setPrototypeOf(target, proto) | Prototype mutation | Object.setPrototypeOf(obj, proto) |
isExtensible(target) | Extensibility check | Object.isExtensible(obj) |
preventExtensions(target) | Prevent extensions | Object.preventExtensions(obj) |
apply(target, thisArg, args) | Function call | fn() (function proxy) |
construct(target, args, newTarget) | new operator | new Fn() (function proxy) |
Reflect API
Reflect provides a method for each Proxy trap, giving you a clean, consistent way to perform default operations:
// Without Reflect — inconsistent behavior
target[prop]; // get
target[prop] = value; // set
delete target[prop]; // delete
prop in target; // has
Object.keys(target); // ownKeys
// With Reflect — consistent, returns success/failure
Reflect.get(target, prop, receiver); // → value
Reflect.set(target, prop, value, receiver); // → boolean
Reflect.deleteProperty(target, prop); // → boolean
Reflect.has(target, prop); // → boolean
Reflect.ownKeys(target); // → string[]Why Reflect Matters in Proxy Handlers
const handler = {
// BAD: bypasses prototype chain and receiver binding
get(target, prop) {
return target[prop];
},
// GOOD: preserves correct this binding and prototype chain
get(target, prop, receiver) {
return Reflect.get(target, prop, receiver);
}
};The receiver parameter ensures that this in getters refers to the proxy, not the raw target — critical for inheritance chains.
Real-World Patterns
Reactive System (Vue 3 Style)
// Simplified reactive system — core concept behind Vue 3 reactivity
let activeEffect = null;
function reactive(obj) {
const deps = new Map(); // prop → Set<effect>
return new Proxy(obj, {
get(target, prop, receiver) {
// Track: record which effect depends on this property
if (activeEffect) {
if (!deps.has(prop)) deps.set(prop, new Set());
deps.get(prop).add(activeEffect);
}
return Reflect.get(target, prop, receiver);
},
set(target, prop, value, receiver) {
const result = Reflect.set(target, prop, value, receiver);
// Trigger: re-run all effects that depend on this property
if (deps.has(prop)) {
deps.get(prop).forEach(effect => effect());
}
return result;
}
});
}
function watchEffect(fn) {
activeEffect = fn;
fn(); // Run once to collect dependencies
activeEffect = null;
}
// Usage
const state = reactive({ count: 0, name: 'Alice' });
watchEffect(() => {
console.log(`Count is: ${state.count}`);
});
state.count++; // Automatically logs "Count is: 1"
state.count++; // Automatically logs "Count is: 2"
state.name = 'Bob'; // No log — this effect doesn't depend on 'name'Validation Layer
function validated(schema) {
return new Proxy({}, {
set(target, prop, value) {
const validator = schema[prop];
if (!validator) {
throw new Error(`Unknown property: ${prop}`);
}
const error = validator(value);
if (error) {
throw new TypeError(`Invalid ${prop}: ${error}`);
}
return Reflect.set(target, prop, value);
}
});
}
const userSchema = {
name: (v) => typeof v !== 'string' ? 'must be a string' : null,
age: (v) => (!Number.isInteger(v) || v < 0 || v > 150) ? 'must be integer 0-150' : null,
email: (v) => (typeof v !== 'string' || !v.includes('@')) ? 'must be valid email' : null,
};
const user = validated(userSchema);
user.name = 'Alice'; // OK
user.age = 30; // OK
user.age = -5; // TypeError: Invalid age: must be integer 0-150
user.unknown = 'x'; // Error: Unknown property: unknownImmutable Objects
function immutable(obj) {
return new Proxy(obj, {
set() {
throw new Error('Cannot modify immutable object');
},
deleteProperty() {
throw new Error('Cannot delete from immutable object');
},
defineProperty() {
throw new Error('Cannot define property on immutable object');
}
});
}
const config = immutable({ apiUrl: 'https://api.example.com', timeout: 5000 });
config.apiUrl; // "https://api.example.com"
config.apiUrl = 'x'; // Error: Cannot modify immutable objectNegative Array Indices
function negativeArray(arr) {
return new Proxy(arr, {
get(target, prop, receiver) {
const index = Number(prop);
if (Number.isInteger(index) && index < 0) {
return Reflect.get(target, target.length + index, receiver);
}
return Reflect.get(target, prop, receiver);
}
});
}
const arr = negativeArray([1, 2, 3, 4, 5]);
arr[-1]; // 5
arr[-2]; // 4API Client with Auto-URL Building
function apiClient(baseUrl) {
const buildPath = (segments) => `${baseUrl}/${segments.join('/')}`;
function createProxy(segments = []) {
return new Proxy(() => {}, {
get(_, prop) {
return createProxy([...segments, prop]);
},
apply(_, __, args) {
const [options = {}] = args;
const url = buildPath(segments);
return fetch(url, {
headers: { 'Content-Type': 'application/json' },
...options,
}).then(r => r.json());
}
});
}
return createProxy();
}
const api = apiClient('https://api.example.com');
// These build URLs automatically:
api.users(); // GET /users
api.users('123').posts(); // GET /users/123/posts
api.users('123').posts({ method: 'POST', body: JSON.stringify({...}) });Access Control / Private Properties
function withAccessControl(obj, privatePrefix = '_') {
return new Proxy(obj, {
get(target, prop, receiver) {
if (typeof prop === 'string' && prop.startsWith(privatePrefix)) {
throw new Error(`Access denied: ${prop} is private`);
}
return Reflect.get(target, prop, receiver);
},
set(target, prop, value, receiver) {
if (typeof prop === 'string' && prop.startsWith(privatePrefix)) {
throw new Error(`Access denied: cannot set private ${prop}`);
}
return Reflect.set(target, prop, value, receiver);
},
ownKeys(target) {
return Reflect.ownKeys(target).filter(
key => typeof key !== 'string' || !key.startsWith(privatePrefix)
);
}
});
}
const obj = withAccessControl({ name: 'Alice', _secret: '12345' });
obj.name; // "Alice"
obj._secret; // Error: Access denied: _secret is private
Object.keys(obj); // ["name"] — _secret is hiddenChange Logging / Audit Trail
function audited(obj) {
const log = [];
const proxy = new Proxy(obj, {
set(target, prop, value) {
log.push({
action: 'set',
prop,
oldValue: target[prop],
newValue: value,
timestamp: Date.now(),
});
return Reflect.set(target, prop, value);
},
deleteProperty(target, prop) {
log.push({
action: 'delete',
prop,
oldValue: target[prop],
timestamp: Date.now(),
});
return Reflect.deleteProperty(target, prop);
}
});
proxy.__audit = () => [...log];
return proxy;
}
const user = audited({ name: 'Alice', role: 'viewer' });
user.role = 'admin';
user.name = 'Bob';
console.log(user.__audit());
// [{ action: 'set', prop: 'role', oldValue: 'viewer', newValue: 'admin', ... },
// { action: 'set', prop: 'name', oldValue: 'Alice', newValue: 'Bob', ... }]Proxy vs Other Approaches
| Approach | Pros | Cons |
|---|---|---|
| Proxy | Intercepts all operations, transparent | Cannot proxy primitives, slight overhead |
Object.defineProperty | Wide support, per-property | Cannot detect new/deleted properties |
| Getter/Setter methods | Explicit, no magic | Verbose, requires discipline |
| Immutable libraries (Immer) | Structural sharing, patches | Dependency, different API |
Performance Considerations
Operation Cost (relative):
Direct property access: 1x
Proxy get (pass-through): ~3-5x
Proxy get (with logic): ~5-10x- Avoid proxying hot-path objects accessed millions of times per second
- Proxy overhead is negligible for typical application logic (state management, form validation, API clients)
- Revocable proxies (
Proxy.revocable()) let you disable a proxy when no longer needed
const { proxy, revoke } = Proxy.revocable(target, handler);
proxy.name; // Works
revoke();
proxy.name; // TypeError: Cannot perform 'get' on a proxy that has been revokedBest Practices
- Always use
Reflectinside trap handlers to preserve correct behavior - Pass
receivertoReflect.get/Reflect.setfor properthisbinding - Return
truefromsettraps (or useReflect.setwhich does this) - Use
Proxy.revocable()for temporary access patterns - Avoid deep nesting of proxies — proxy only the objects you need to observe
- Proxy is not polyfillable — ensure your target environments support it