Advanced Proxy Patterns
This section covers a few more Proxy patterns: observable objects, revocable proxies, and default values.
Observable Object
function observable(obj) {
const listeners = new Map();
return new Proxy(obj, {
set(target, property, value, receiver) {
const oldValue = target[property];
const result = Reflect.set(target, property, value, receiver);
if (oldValue !== value && listeners.has(property)) {
listeners.get(property).forEach((fn) => fn(value, oldValue));
}
return result;
},
get(target, property, receiver) {
if (property === "on") {
return (prop, callback) => {
if (!listeners.has(prop)) {
listeners.set(prop, []);
}
listeners.get(prop).push(callback);
};
}
return Reflect.get(target, property, receiver);
},
});
}
const state = observable({ count: 0 });
state.on("count", (newVal, oldVal) => {
console.log(`count changed from ${oldVal} to ${newVal}`);
});
state.count = 1; // Logs: "count changed from 0 to 1"
state.count = 2; // Logs: "count changed from 1 to 2"
Revocable Proxy
Create a proxy that can be disabled:
const { proxy, revoke } = Proxy.revocable(
{ x: 1 },
{
get(target, property) {
return target[property];
},
},
);
console.log(proxy.x); // 1
revoke(); // Disable the proxy
// console.log(proxy.x); // TypeError: Cannot perform 'get' on a proxy that has been revoked
Default Values
function withDefaults(target, defaults) {
return new Proxy(target, {
get(target, property, receiver) {
const value = Reflect.get(target, property, receiver);
if (value === undefined && property in defaults) {
return defaults[property];
}
return value;
},
});
}
const config = withDefaults(
{ port: 3000 },
{ host: "localhost", port: 8080, debug: false },
);
console.log(config.port); // 3000 (from target)
console.log(config.host); // "localhost" (from defaults)
console.log(config.debug); // false (from defaults)
Performance Considerations
Proxies add overhead to every intercepted operation. Avoid using them:
- In hot code paths
- On objects accessed in tight loops
- When simpler solutions exist
Use a Proxy when what you get from it is worth that overhead. Validation, logging, and reactivity are the usual reasons.
Summary
| Feature | Purpose |
|---|---|
Proxy |
Intercept and customize object operations |
Reflect |
Perform default operations, mirrors Proxy traps |
| Traps | Handler methods that intercept specific operations |
Proxy.revocable() |
Create a proxy that can be disabled |
Common use cases:
- Validation and type checking
- Logging and debugging
- Observable/reactive objects
- Virtual properties and default values
- Access control