Proxy and Reflect API
Answer
Proxy allows you to intercept and customize fundamental operations on objects (property access, assignment, function calls). Reflect provides methods for these same operations, making it easy to forward default behavior.
Proxy Basics
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);
proxy.name; // Logs: "Getting name", returns "John"
proxy.age = 31; // Logs: "Setting age = 31"
Common Traps
| Trap | Triggered By |
|---|---|
get | Property read |
set | Property assignment |
has | in operator |
deleteProperty | delete operator |
apply | Function call |
construct | new operator |
getPrototypeOf | Object.getPrototypeOf() |
ownKeys | Object.keys(), for...in |
Validation
const validator = {
set(target, property, value) {
if (property === "age") {
if (typeof value !== "number") {
throw new TypeError("Age must be a number");
}
if (value < 0 || value > 150) {
throw new RangeError("Invalid age");
}
}
target[property] = value;
return true;
},
};
const person = new Proxy({}, validator);
person.age = 30; // OK
person.age = -5; // RangeError: Invalid age
person.age = "30"; // TypeError: Age must be a number
Default Values
const withDefaults = (target, defaults) => {
return new Proxy(target, {
get(target, property) {
return property in target ? target[property] : defaults[property];
},
});
};
const settings = withDefaults(
{},
{
theme: "dark",
fontSize: 14,
language: "en",
}
);
console.log(settings.theme); // "dark"
console.log(settings.fontSize); // 14
settings.theme = "light";
console.log(settings.theme); // "light"
Logging / Debugging
function createLogger(target) {
return new Proxy(target, {
get(target, property, receiver) {
const value = Reflect.get(target, property, receiver);
console.log(`[GET] ${property} = ${JSON.stringify(value)}`);
return value;
},
set(target, property, value, receiver) {
console.log(`[SET] ${property} = ${JSON.stringify(value)}`);
return Reflect.set(target, property, value, receiver);
},
deleteProperty(target, property) {
console.log(`[DELETE] ${property}`);
return Reflect.deleteProperty(target, property);
},
});
}
const user = createLogger({ name: "John" });
user.name; // [GET] name = "John"
user.age = 30; // [SET] age = 30
delete user.age; // [DELETE] age
Reflect API
// Reflect provides the same operations as Proxy traps
// Makes it easy to forward to default behavior
const obj = { name: "John" };
// Old way
obj.name; // "John"
obj.name = "Jane"; // assignment
"name" in obj; // true
delete obj.age; // true
// Reflect way
Reflect.get(obj, "name"); // "John"
Reflect.set(obj, "name", "Jane"); // true
Reflect.has(obj, "name"); // true
Reflect.deleteProperty(obj, "age"); // true
// Why use Reflect?
// 1. Returns boolean instead of throwing
// 2. Consistent API for metaprogramming
// 3. Works properly with Proxy
Observable Objects
function observable(target, callback) {
return new Proxy(target, {
set(target, property, value, receiver) {
const oldValue = target[property];
const result = Reflect.set(target, property, value, receiver);
if (oldValue !== value) {
callback(property, oldValue, value);
}
return result;
},
});
}
const data = observable({ count: 0 }, (prop, oldVal, newVal) => {
console.log(`${prop} changed from ${oldVal} to ${newVal}`);
updateUI();
});
data.count = 1; // "count changed from 0 to 1"
data.count = 2; // "count changed from 1 to 2"
Private Properties
function createPrivate(target, privateKeys = []) {
return new Proxy(target, {
get(target, property) {
if (privateKeys.includes(property)) {
throw new Error(`Cannot access private property: ${property}`);
}
return Reflect.get(target, property);
},
set(target, property, value) {
if (privateKeys.includes(property)) {
throw new Error(`Cannot set private property: ${property}`);
}
return Reflect.set(target, property, value);
},
ownKeys(target) {
return Reflect.ownKeys(target).filter(
(key) => !privateKeys.includes(key)
);
},
});
}
const user = createPrivate({ name: "John", _password: "secret" }, [
"_password",
]);
console.log(user.name); // "John"
console.log(user._password); // Error: Cannot access private property
console.log(Object.keys(user)); // ["name"]
Function Proxy
const loggedFunction = new Proxy(originalFunction, {
apply(target, thisArg, args) {
console.log(`Calling ${target.name} with:`, args);
const result = Reflect.apply(target, thisArg, args);
console.log(`Result:`, result);
return result;
},
});
// Memoization via Proxy
function memoize(fn) {
const cache = new Map();
return new Proxy(fn, {
apply(target, thisArg, args) {
const key = JSON.stringify(args);
if (cache.has(key)) {
console.log("Cache hit");
return cache.get(key);
}
const result = Reflect.apply(target, thisArg, args);
cache.set(key, result);
return result;
},
});
}
const expensiveCalc = memoize((n) => {
console.log("Computing...");
return n * n;
});
expensiveCalc(5); // Computing... 25
expensiveCalc(5); // Cache hit 25
Revocable Proxy
// Can be revoked after creation
const { proxy, revoke } = Proxy.revocable(target, handler);
proxy.name; // Works
revoke(); // Revoke access
proxy.name; // TypeError: Cannot perform 'get' on a proxy that has been revoked
// Use case: Temporary access
function grantTemporaryAccess(data, duration) {
const { proxy, revoke } = Proxy.revocable(data, {});
setTimeout(revoke, duration);
return proxy;
}
const tempAccess = grantTemporaryAccess(secretData, 5000);
// Access revoked after 5 seconds
Key Points
- Proxy intercepts fundamental object operations
- Reflect provides matching methods for default behavior
- Use Proxies for validation, logging, caching
- Revocable proxies for temporary access
- Performance: Proxies add overhead
- Always use Reflect in handlers for correctness