|$ curl https://forge-ai.dev/api/markdown?path=docs/js/proxies
$cat docs/javascript-—-proxies-&-reflect.md
updated This week·30 min read·published

JavaScript — Proxies & Reflect

JavaScriptProxiesReflectAdvancedAdvanced
Introduction

The Proxy object enables you to intercept and customize fundamental operations on another object — property lookup, assignment, enumeration, function invocation, and more. Proxies create a layer of indirection that can trap operations and redefine their behavior without modifying the target object itself.

Introduced in ES2015, proxies pair naturally with the Reflect API, which provides methods that mirror each proxy trap. Using Reflect inside traps ensures default behavior is preserved while allowing custom logic to run before or after.

This page covers every proxy trap, the Reflect API, common use cases like validation and caching, and performance considerations.

proxy-intro.js
JavaScript
1// A Proxy wraps a target object and traps operations
2const target = { message: "hello" };
3
4const handler = {
5 get(target, property, receiver) {
6 console.log(`Getting property: ${property}`);
7 return Reflect.get(target, property, receiver);
8 }
9};
10
11const proxy = new Proxy(target, handler);
12
13console.log(proxy.message);
14// Getting property: message
15// "hello"
16
17// The original target remains unmodified
18console.log(target.message); // "hello"
Proxy Traps

Traps are handler methods that intercept operations on the proxy. JavaScript defines 13 traps covering get/set, property enumeration, function invocation, prototype operations, and more. Each trap corresponds to a Reflect method of the same name.

TrapInterceptsTriggered By
getProperty accessproxy[key], proxy.key
setProperty assignmentproxy[key] = val
hasin operator"key" in proxy
deletePropertydelete operatordelete proxy[key]
ownKeysObject.keys/entriesObject.keys(proxy)
getOwnPropertyDescriptorProperty descriptorsObject.getOwnPropertyDescriptor(proxy, key)
definePropertyObject.definePropertyObject.defineProperty(proxy, key, desc)
preventExtensionsObject.preventExtensionsObject.preventExtensions(proxy)
isExtensibleObject.isExtensibleObject.isExtensible(proxy)
getPrototypeOfObject.getPrototypeOfObject.getPrototypeOf(proxy)
setPrototypeOfObject.setPrototypeOfObject.setPrototypeOf(proxy, proto)
applyFunction callproxy(), proxy.apply()
constructnew operatornew proxy()
proxy-traps.js
JavaScript
1// Comprehensive trap examples
2
3const handler = {
4 // Property access
5 get(target, prop, receiver) {
6 console.log(`GET ${String(prop)}`);
7 return prop in target ? Reflect.get(target, prop, receiver) : "fallback";
8 },
9
10 // Property assignment
11 set(target, prop, value, receiver) {
12 console.log(`SET ${String(prop)} = ${value}`);
13 return Reflect.set(target, prop, value, receiver);
14 },
15
16 // in operator
17 has(target, prop) {
18 console.log(`HAS ${String(prop)}`);
19 return Reflect.has(target, prop);
20 },
21
22 // delete operator
23 deleteProperty(target, prop) {
24 console.log(`DELETE ${String(prop)}`);
25 return Reflect.deleteProperty(target, prop);
26 },
27
28 // Object.keys / for..in
29 ownKeys(target) {
30 console.log("OWN_KEYS");
31 return Reflect.ownKeys(target);
32 }
33};
34
35const obj = { a: 1, b: 2 };
36const proxy = new Proxy(obj, handler);
37
38proxy.a; // GET a
39proxy.c; // GET c → "fallback"
40proxy.a = 10; // SET a = 10
41"a" in proxy; // HAS a
42delete proxy.a; // DELETE a
43Object.keys(proxy); // OWN_KEYS
Reflect API

The Reflect object provides static methods that correspond to every proxy trap. They perform the default operation for each trap, making them the standard way to forward operations inside handler functions. Using Reflect instead of direct operations ensures consistent behavior with the target object's internal slots.

reflect-api.js
JavaScript
1// Reflect methods mirror proxy traps exactly
2const target = { x: 1, y: 2 };
3
4// Reflect.get — same as target[prop]
5console.log(Reflect.get(target, "x")); // 1
6
7// Reflect.set — returns boolean success
8console.log(Reflect.set(target, "x", 42)); // true
9console.log(target.x); // 42
10
11// Reflect.has — same as "in" operator
12console.log(Reflect.has(target, "y")); // true
13
14// Reflect.ownKeys — returns all own keys
15console.log(Reflect.ownKeys(target)); // ["x", "y"]
16
17// Reflect.deleteProperty — returns boolean
18console.log(Reflect.deleteProperty(target, "y")); // true
19
20// Reflect.defineProperty
21console.log(Reflect.defineProperty(target, "z", {
22 value: 3,
23 writable: true
24})); // true
25
26// Essential: Reflect in proxy handlers preserves receiver binding
27// Without Reflect: issues with inheritance and getters
28const parent = {
29 get value() { return this._val; }
30};
31
32const child = new Proxy(parent, {
33 get(target, prop, receiver) {
34 // Must use Reflect.get to pass the correct receiver
35 return Reflect.get(target, prop, receiver);
36 }
37});
38
39child._val = "child";
40console.log(child.value); // "child" — correct receiver

info

Always use Reflect methods inside proxy traps rather than performing the operation directly (e.g., target[prop] instead of Reflect.get(target, prop, receiver)). The receiver parameter is critical for correctly handling getters and inheritance — target[prop] ignores it entirely.
Use Cases

Validation

Proxies can enforce type constraints, range limits, and schema validation on object properties without cluttering the business logic with validation code.

validation.js
JavaScript
1// Schema validation with Proxy
2function createValidatedObject(schema) {
3 return new Proxy({}, {
4 set(target, prop, value) {
5 const rules = schema[prop];
6 if (rules) {
7 if (rules.type && typeof value !== rules.type) {
8 throw new TypeError(`Expected ${prop} to be ${rules.type}`);
9 }
10 if (rules.min !== undefined && value < rules.min) {
11 throw new RangeError(`${prop} must be >= ${rules.min}`);
12 }
13 if (rules.max !== undefined && value > rules.max) {
14 throw new RangeError(`${prop} must be <= ${rules.max}`);
15 }
16 }
17 return Reflect.set(target, prop, value);
18 }
19 });
20}
21
22const user = createValidatedObject({
23 name: { type: "string" },
24 age: { type: "number", min: 0, max: 150 }
25});
26
27user.name = "Alice"; // OK
28user.age = 25; // OK
29// user.age = -1; // RangeError: age must be >= 0
30// user.age = "old"; // TypeError: Expected age to be number

Logging & Observability

Proxies can automatically log all property access, mutations, and method calls — invaluable for debugging, auditing, and tracing data flow.

logging.js
JavaScript
1// Observable object — logs all operations
2function observable(target) {
3 const handlers = [];
4
5 const proxy = new Proxy(target, {
6 get(target, prop, receiver) {
7 if (prop === "onChange") return (fn) => handlers.push(fn);
8 const value = Reflect.get(target, prop, receiver);
9 if (typeof value === "function") {
10 return (...args) => {
11 const result = value.apply(target, args);
12 handlers.forEach(h => h(prop, args, result));
13 return result;
14 };
15 }
16 handlers.forEach(h => h("GET", prop, value));
17 return value;
18 },
19 set(target, prop, value, receiver) {
20 const old = target[prop];
21 const result = Reflect.set(target, prop, value, receiver);
22 handlers.forEach(h => h("SET", prop, { from: old, to: value }));
23 return result;
24 }
25 });
26
27 return proxy;
28}
29
30const data = observable({ count: 0 });
31data.onChange((action, prop, detail) => {
32 console.log(`[${new Date().toISOString()}]`, action, prop, detail);
33});
34
35data.count = 1; // [timestamp] SET count { from: 0, to: 1 }
36data.count = 2; // [timestamp] SET count { from: 1, to: 2 }
37console.log(data.count); // [timestamp] GET count 2

Caching & Memoization

Proxy-based caching can transparently memoize computed values and cache results with configurable TTL, all without modifying the original object.

caching.js
JavaScript
1// Transparent caching proxy
2function withCache(target, ttlMs = 5000) {
3 const cache = new Map();
4
5 return new Proxy(target, {
6 get(target, prop, receiver) {
7 if (typeof target[prop] !== "function") {
8 const cached = cache.get(prop);
9 if (cached && Date.now() < cached.expires) {
10 console.log(`Cache HIT for ${String(prop)}`);
11 return cached.value;
12 }
13 const value = Reflect.get(target, prop, receiver);
14 cache.set(prop, { value, expires: Date.now() + ttlMs });
15 return value;
16 }
17 return (...args) => {
18 const key = JSON.stringify([prop, args]);
19 const cached = cache.get(key);
20 if (cached && Date.now() < cached.expires) {
21 console.log(`Cache HIT for ${String(prop)}()`);
22 return cached.value;
23 }
24 const result = target[prop](...args);
25 cache.set(key, { value: result, expires: Date.now() + ttlMs });
26 return result;
27 };
28 }
29 });
30}
31
32const math = withCache({
33 fib: (n) => n <= 1 ? n : math.fib(n - 1) + math.fib(n - 2)
34});
35
36console.log(math.fib(40)); // Computed (slow)
37console.log(math.fib(40)); // Cache HIT — instant
preview
🔥

pro tip

For caching proxies, consider using a WeakMap instead of Map to avoid preventing garbage collection of cached objects. For production caching with proxies, evaluate tradeoffs — V8 optimizations are disabled for proxied objects, so a dedicated cache layer may perform better for hot paths.
Revocable Proxies

Proxy.revocable creates a proxy that can be revoked (disabled) at any point. Once revoked, any operation on the proxy throws a TypeError. This is useful for providing controlled access to resources that can be revoked after a timeout or when a user logs out.

revocable.js
JavaScript
1// Revocable proxy — access can be revoked
2const resource = { secret: "top-secret-data" };
3
4const { proxy, revoke } = Proxy.revocable(resource, {
5 get(target, prop, receiver) {
6 if (prop === "secret") {
7 console.warn("Accessing secret data");
8 }
9 return Reflect.get(target, prop, receiver);
10 }
11});
12
13console.log(proxy.secret); // Accessing secret data → "top-secret-data"
14
15// Revoke access
16revoke();
17
18// Any subsequent operation throws
19try {
20 console.log(proxy.secret);
21} catch (e) {
22 console.log(e.message); // Cannot perform 'get' on a proxy that has been revoked
23}
24
25// Practical: revoke after timeout
26function createTimedAccess(data, ms) {
27 const { proxy, revoke } = Proxy.revocable(data, {});
28 setTimeout(revoke, ms);
29 return proxy;
30}
31
32const tempAccess = createTimedAccess({ token: "abc" }, 5000);
33console.log(tempAccess.token); // "abc" (within 5 seconds)
34// After 5 seconds: TypeError

warning

Revocable proxies are one-directional — once revoked, there is no way to re-enable them. You must create a new proxy to restore access. This makes them ideal for temporary access patterns (API tokens, session-scoped objects) but unsuitable for toggle patterns.
Performance Considerations

Proxies introduce overhead because every trapped operation must go through the handler. V8 and other engines cannot apply their normal optimizations (inline caching, hidden classes) to proxy access. For hot-path code — tight loops, frequently accessed properties, performance-critical algorithms — proxies can cause significant degradation.

Proxy traps disable V8 hidden class optimizations — access is 10-100x slower than direct property access
Avoid proxying objects in hot loops or frequently called functions
Use proxies at API boundaries (validation layer, logging middleware) rather than in inner loops
Revocable proxies have the same performance characteristics as regular proxies
Nested proxies compound the slowdown — each layer adds handler dispatch overhead
Profile before and after — the overhead may be acceptable for non-critical paths
Consider using Object.defineProperty for simple getter/setter scenarios instead of Proxy
perf-comparison.js
JavaScript
1// Performance comparison
2const target = { x: 1, y: 2, z: 3 };
3const proxy = new Proxy(target, {
4 get(t, p, r) { return Reflect.get(t, p, r); }
5});
6
7const ITERATIONS = 10_000_000;
8
9// Direct access — optimized by V8
10console.time("direct");
11let sum = 0;
12for (let i = 0; i < ITERATIONS; i++) {
13 sum += target.x + target.y + target.z;
14}
15console.timeEnd("direct");
16
17// Proxy access — significantly slower
18console.time("proxy");
19sum = 0;
20for (let i = 0; i < ITERATIONS; i++) {
21 sum += proxy.x + proxy.y + proxy.z;
22}
23console.timeEnd("proxy");
24
25// On V8: direct ~5-10ms, proxy ~200-500ms
26// Proxy is 20-100x slower for property access in tight loops

best practice

Use proxies at architectural boundaries — not in implementation details. A validation proxy wrapping an API payload is fine. A proxy wrapping every item in a 10,000-element array that gets iterated every frame is not. Measure the impact and use proxies where the flexibility justifies the cost.
Best Practices
Always return Reflect.<trap>(...) from handlers to preserve default behavior
Pass the receiver argument to Reflect.get and Reflect.set for correct inheritance
Use Proxy.revocable for temporary or session-scoped access to resources
Keep handler functions lean — avoid heavy computation inside traps
Do not proxy the same target object multiple times unnecessarily
Prefer simple getter/setter via defineProperty when you only need 1-2 properties intercepted
Use proxies for cross-cutting concerns (logging, validation, caching) not core business logic
Document proxy behavior clearly — it can make code harder to debug and reason about
🔥

pro tip

Debugging proxies can be difficult because the stack traces and console output hide the proxy layer. Use console.log(target) (the original object) alongside proxy operations to verify state. In Chrome DevTools, proxied objects display a "Proxy" badge — click through to inspect the handler and target separately.
$Blueprint — Engineering Documentation·Section ID: JS-PROXIES·Revision: 1.0