Proxy & Reflect
Metaprogramming with Proxy objects and the Reflect API
Introduction to Metaprogramming
Metaprogramming is writing code that manipulates or intercepts other code at runtime.
JavaScript's Proxy and Reflect APIs enable powerful metaprogramming patterns
like validation, logging, virtual objects, and reactive systems.
Key Concepts
- Proxy — Wraps an object to intercept and customize operations
- Handler — Object containing trap methods
- Trap — Method that intercepts an operation (get, set, etc.)
- Reflect — API mirroring Proxy traps with default behaviors
Basic Proxy
// Syntax: new Proxy(target, handler)
const target = {
name: "Alice",
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; // Must return true for success
}
};
const proxy = new Proxy(target, handler);
proxy.name; // Logs: "Getting name", returns "Alice"
proxy.age = 31; // Logs: "Setting age to 31"
console.log(proxy.age); // 31
Common Proxy Traps
| Trap | Intercepts |
|---|---|
get(target, prop) |
Property access: obj.prop |
set(target, prop, value) |
Property assignment: obj.prop = value |
has(target, prop) |
in operator |
deleteProperty(target, prop) |
delete operator |
apply(target, thisArg, args) |
Function calls |
construct(target, args) |
new operator |
Validation Proxy
function createValidatedObject(schema) {
return new Proxy({}, {
set(target, property, value) {
const validator = schema[property];
if (!validator) {
throw new Error(`Unknown property: ${property}`);
}
if (!validator(value)) {
throw new Error(`Invalid value for ${property}: ${value}`);
}
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 = "Alice"; // ✓ OK
user.age = 30; // ✓ OK
user.age = -5; // ✗ Error: Invalid value for age
user.foo = "bar"; // ✗ Error: Unknown property: foo
Default Values & Virtual Properties
// Auto-create missing properties
const withDefaults = new Proxy({}, {
get(target, property) {
if (!(property in target)) {
target[property] = 0; // Default value
}
return target[property];
}
});
console.log(withDefaults.count); // 0 (auto-created)
withDefaults.count++;
console.log(withDefaults.count); // 1
// Virtual (computed) properties
const user = {
firstName: "John",
lastName: "Doe"
};
const userProxy = new Proxy(user, {
get(target, property) {
if (property === "fullName") {
return `${target.firstName} ${target.lastName}`;
}
return target[property];
}
});
console.log(userProxy.fullName); // "John Doe"
The Reflect API
Reflect provides default implementations for Proxy traps:
// Reflect mirrors Proxy traps
const obj = { x: 1, y: 2 };
// Instead of:
obj.x; // Get
obj.x = 10; // Set
"x" in obj; // Has
delete obj.x; // Delete
// Use Reflect:
Reflect.get(obj, "x"); // Get
Reflect.set(obj, "x", 10); // Set
Reflect.has(obj, "x"); // Has
Reflect.deleteProperty(obj, "x"); // Delete
// Useful in Proxy handlers to call default behavior
const loggingProxy = new Proxy(obj, {
get(target, property, receiver) {
console.log(`Accessing ${property}`);
return Reflect.get(target, property, receiver); // Default behavior
},
set(target, property, value, receiver) {
console.log(`Setting ${property} = ${value}`);
return Reflect.set(target, property, value, receiver);
}
});
Reactive Proxy (Vue-style)
function reactive(obj, onChange) {
return new Proxy(obj, {
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;
}
});
}
const state = reactive({ count: 0 }, (prop, newVal, oldVal) => {
console.log(`${prop} changed: ${oldVal} → ${newVal}`);
// Re-render UI, etc.
});
state.count = 1; // "count changed: 0 → 1"
state.count = 2; // "count changed: 1 → 2"
Function Proxy
// Intercept function calls
function createLoggedFunction(fn) {
return new Proxy(fn, {
apply(target, thisArg, args) {
console.log(`Calling ${fn.name} with:`, args);
const result = Reflect.apply(target, thisArg, args);
console.log(`Result:`, result);
return result;
}
});
}
const add = (a, b) => a + b;
const loggedAdd = createLoggedFunction(add);
loggedAdd(2, 3);
// Calling add with: [2, 3]
// Result: 5
// Intercept constructor calls
class User {
constructor(name) {
this.name = name;
}
}
const TrackedUser = new Proxy(User, {
construct(target, args) {
console.log("Creating user:", args[0]);
return Reflect.construct(target, args);
}
});
new TrackedUser("Alice"); // "Creating user: Alice"
Revocable Proxies
// Create a proxy that can be disabled
const { proxy, revoke } = Proxy.revocable(
{ secret: "password123" },
{
get(target, prop) {
return target[prop];
}
}
);
console.log(proxy.secret); // "password123"
// Revoke access
revoke();
console.log(proxy.secret); // TypeError: Cannot perform 'get' on a revoked proxy
⚠️ Considerations
- Performance: Proxies add overhead; avoid in hot paths
- Identity: proxy !== target, which can cause issues
- Built-ins: Some objects (Map, Set) need special handling
- Invariants: Traps must respect JavaScript invariants
📚 Learn More
💡 Key Takeaways
- • Proxy intercepts fundamental operations on objects
- • Use traps (get, set, has, etc.) to customize behavior
- • Reflect provides default implementations for trap operations
- • Great for validation, logging, reactive systems, and access control
- • Revocable proxies allow disabling access to data