Advanced JavaScript Proxies Complete Guide
Master JavaScript Proxy objects for meta-programming. Covers all 13 proxy traps, handler invariants, revocable proxies, proxy chains, validation layers, virtual object patterns, lazy initialization, access control, and performance considerations for production use.
JavaScript's Proxy object intercepts and customizes fundamental operations on objects. It enables powerful meta-programming patterns including validation, access control, lazy initialization, and virtual properties through 13 handler traps that correspond to internal object operations.
For how Proxy works alongside the Reflect API, see Using Reflect and Proxy Together in JavaScript.
The 13 Proxy Traps
// Every operation on an object has a corresponding proxy trap
// Here is the complete handler with all 13 traps:
const completeHandler = {
// 1. Property read: obj.prop, obj['prop']
get(target, property, receiver) {
console.log(`get: ${String(property)}`);
return Reflect.get(target, property, receiver);
},
// 2. Property write: obj.prop = value
set(target, property, value, receiver) {
console.log(`set: ${String(property)} = ${value}`);
return Reflect.set(target, property, value, receiver);
},
// 3. Property existence: 'prop' in obj
has(target, property) {
console.log(`has: ${String(property)}`);
return Reflect.has(target, property);
},
// 4. Property deletion: delete obj.prop
deleteProperty(target, property) {
console.log(`delete: ${String(property)}`);
return Reflect.deleteProperty(target, property);
},
// 5. Own property keys: Object.keys, Object.getOwnPropertyNames, for..in
ownKeys(target) {
console.log("ownKeys");
return Reflect.ownKeys(target);
},
// 6. Property descriptor: Object.getOwnPropertyDescriptor(obj, prop)
getOwnPropertyDescriptor(target, property) {
console.log(`getOwnPropertyDescriptor: ${String(property)}`);
return Reflect.getOwnPropertyDescriptor(target, property);
},
// 7. Define property: Object.defineProperty(obj, prop, descriptor)
defineProperty(target, property, descriptor) {
console.log(`defineProperty: ${String(property)}`);
return Reflect.defineProperty(target, property, descriptor);
},
// 8. Prototype read: Object.getPrototypeOf(obj)
getPrototypeOf(target) {
console.log("getPrototypeOf");
return Reflect.getPrototypeOf(target);
},
// 9. Prototype write: Object.setPrototypeOf(obj, proto)
setPrototypeOf(target, prototype) {
console.log("setPrototypeOf");
return Reflect.setPrototypeOf(target, prototype);
},
// 10. Extensibility check: Object.isExtensible(obj)
isExtensible(target) {
console.log("isExtensible");
return Reflect.isExtensible(target);
},
// 11. Prevent extensions: Object.preventExtensions(obj)
preventExtensions(target) {
console.log("preventExtensions");
return Reflect.preventExtensions(target);
},
// 12. Function call: proxy(args)
apply(target, thisArg, argumentsList) {
console.log(`apply with ${argumentsList.length} args`);
return Reflect.apply(target, thisArg, argumentsList);
},
// 13. Constructor call: new proxy(args)
construct(target, argumentsList, newTarget) {
console.log(`construct with ${argumentsList.length} args`);
return Reflect.construct(target, argumentsList, newTarget);
}
};
// Usage: wrap any object with all 13 traps
const target = { x: 1, y: 2 };
const proxy = new Proxy(target, completeHandler);
proxy.x; // get: x
proxy.z = 3; // set: z = 3
"x" in proxy; // has: x
delete proxy.z; // delete: z
Object.keys(proxy); // ownKeysValidation and Type Enforcement
// Proxy enables runtime type checking on object properties
function createTypedObject(schema) {
const data = {};
return new Proxy(data, {
set(target, property, value) {
const validator = schema[property];
if (!validator) {
throw new TypeError(`Unknown property: ${property}`);
}
const result = validator(value);
if (result !== true) {
throw new TypeError(
`Invalid value for ${property}: ${result || "validation failed"}`
);
}
target[property] = value;
return true;
},
get(target, property) {
if (property in target) return target[property];
if (property in schema) return undefined; // Known but unset
throw new TypeError(`Unknown property: ${property}`);
}
});
}
// Schema definition with validators
const userSchema = {
name: (v) => typeof v === "string" && v.length > 0 || "must be a non-empty string",
age: (v) => Number.isInteger(v) && v >= 0 && v <= 150 || "must be an integer 0-150",
email: (v) => typeof v === "string" && v.includes("@") || "must contain @",
role: (v) => ["admin", "user", "editor"].includes(v) || "must be admin, user, or editor"
};
const user = createTypedObject(userSchema);
user.name = "Alice"; // OK
user.age = 30; // OK
user.email = "a@b.com"; // OK
// user.age = -5; // TypeError: must be an integer 0-150
// user.unknown = "x"; // TypeError: Unknown property: unknown
// IMMUTABLE OBJECT PATTERN
function createImmutable(obj) {
return new Proxy(obj, {
set() {
throw new TypeError("Cannot modify an immutable object");
},
deleteProperty() {
throw new TypeError("Cannot delete from an immutable object");
},
defineProperty() {
throw new TypeError("Cannot define properties on an immutable object");
},
setPrototypeOf() {
throw new TypeError("Cannot change prototype of an immutable object");
},
get(target, property, receiver) {
const value = Reflect.get(target, property, receiver);
// Deep immutability: wrap nested objects too
if (typeof value === "object" && value !== null) {
return createImmutable(value);
}
return value;
}
});
}
const config = createImmutable({
db: { host: "localhost", port: 5432 },
cache: { ttl: 3600 }
});
console.log(config.db.host); // "localhost"
// config.db.host = "remote"; // TypeError: Cannot modify an immutable objectVirtual Objects and Lazy Initialization
// VIRTUAL PROPERTIES: Properties that don't exist on the target
// but are computed on access
function createComputedObject(data, computedProps) {
return new Proxy(data, {
get(target, property, receiver) {
if (property in computedProps) {
return computedProps[property](target);
}
return Reflect.get(target, property, receiver);
},
has(target, property) {
return property in computedProps || Reflect.has(target, property);
},
ownKeys(target) {
return [...Reflect.ownKeys(target), ...Object.keys(computedProps)];
},
getOwnPropertyDescriptor(target, property) {
if (property in computedProps) {
return {
configurable: true,
enumerable: true,
value: computedProps[property](target),
writable: false
};
}
return Reflect.getOwnPropertyDescriptor(target, property);
}
});
}
const rectangle = createComputedObject(
{ width: 10, height: 5 },
{
area: (t) => t.width * t.height,
perimeter: (t) => 2 * (t.width + t.height),
diagonal: (t) => Math.sqrt(t.width ** 2 + t.height ** 2)
}
);
console.log(rectangle.area); // 50
console.log(rectangle.perimeter); // 30
console.log(rectangle.diagonal); // 11.18...
rectangle.width = 20;
console.log(rectangle.area); // 100 (recomputed)
// LAZY INITIALIZATION PATTERN
function createLazy(factory) {
let instance = null;
return new Proxy({}, {
get(target, property, receiver) {
if (!instance) {
instance = factory();
console.log("Lazy instance created");
}
return Reflect.get(instance, property, receiver);
},
set(target, property, value, receiver) {
if (!instance) {
instance = factory();
console.log("Lazy instance created");
}
return Reflect.set(instance, property, value, receiver);
}
});
}
const expensiveService = createLazy(() => {
// Only created when first accessed
return {
data: loadExpensiveData(),
process(input) { return input; }
};
});
function loadExpensiveData() {
return { items: [1, 2, 3] };
}
// No instance created yet
console.log("Service defined but not initialized");
// First access triggers creation
console.log(expensiveService.data); // "Lazy instance created" then { items: [1,2,3] }Revocable Proxies and Access Control
// REVOCABLE PROXIES: Can be "turned off" to prevent further access
function createSecureSession(data, expiresInMs) {
const { proxy, revoke } = Proxy.revocable(data, {
get(target, property) {
console.log(`[Session] Reading: ${String(property)}`);
return Reflect.get(target, property);
},
set(target, property, value) {
console.log(`[Session] Writing: ${String(property)}`);
return Reflect.set(target, property, value);
}
});
// Auto-revoke after expiration
setTimeout(() => {
revoke();
console.log("[Session] Expired and revoked");
}, expiresInMs);
return { proxy, revoke };
}
const session = createSecureSession(
{ userId: 123, role: "admin" },
5000 // 5 second session
);
session.proxy.userId; // OK: 123
// After 5 seconds: session.proxy.userId throws TypeError
// ROLE-BASED ACCESS CONTROL
function createAccessControlled(data, permissions) {
return new Proxy(data, {
get(target, property, receiver) {
const perm = permissions[property];
if (perm && !perm.read) {
throw new Error(`Access denied: cannot read '${String(property)}'`);
}
return Reflect.get(target, property, receiver);
},
set(target, property, value, receiver) {
const perm = permissions[property];
if (perm && !perm.write) {
throw new Error(`Access denied: cannot write '${String(property)}'`);
}
return Reflect.set(target, property, value, receiver);
},
deleteProperty(target, property) {
const perm = permissions[property];
if (perm && !perm.delete) {
throw new Error(`Access denied: cannot delete '${String(property)}'`);
}
return Reflect.deleteProperty(target, property);
},
ownKeys(target) {
// Only expose readable properties
return Reflect.ownKeys(target).filter((key) => {
const perm = permissions[key];
return !perm || perm.read !== false;
});
}
});
}
const sensitiveData = createAccessControlled(
{ name: "Alice", ssn: "123-45-6789", email: "alice@example.com" },
{
ssn: { read: false, write: false, delete: false },
name: { read: true, write: true, delete: false },
email: { read: true, write: true, delete: false }
}
);
console.log(sensitiveData.name); // "Alice"
// sensitiveData.ssn; // Error: Access denied
console.log(Object.keys(sensitiveData)); // ["name", "email"] (ssn hidden)Proxy Chains and Composition
// Stack multiple proxies for layered behavior
function withLogging(target) {
return new Proxy(target, {
get(target, property, receiver) {
const value = Reflect.get(target, property, receiver);
console.log(`[LOG] get ${String(property)} -> ${JSON.stringify(value)}`);
return value;
},
set(target, property, value, receiver) {
console.log(`[LOG] set ${String(property)} = ${JSON.stringify(value)}`);
return Reflect.set(target, property, value, receiver);
}
});
}
function withCache(target, ttlMs = 5000) {
const cache = new Map();
return new Proxy(target, {
get(target, property, receiver) {
const cached = cache.get(property);
if (cached && Date.now() - cached.time < ttlMs) {
return cached.value;
}
const value = Reflect.get(target, property, receiver);
cache.set(property, { value, time: Date.now() });
return value;
},
set(target, property, value, receiver) {
cache.delete(property);
return Reflect.set(target, property, value, receiver);
}
});
}
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;
}
});
}
// Compose proxies: logging -> caching -> defaults -> target
const data = { name: "Alice" };
const withDef = withDefaults(data, { role: "user", theme: "dark" });
const cached = withCache(withDef, 3000);
const logged = withLogging(cached);
console.log(logged.name); // [LOG] get name -> "Alice"
console.log(logged.role); // [LOG] get role -> "user" (default)
console.log(logged.theme); // [LOG] get theme -> "dark" (default)| Trap | Triggered By | Must Return |
|---|---|---|
get | Property read, Reflect.get | Any value |
set | Property write, Reflect.set | Boolean (true = success) |
has | in operator, Reflect.has | Boolean |
deleteProperty | delete operator | Boolean |
ownKeys | Object.keys, for...in | Array of strings/symbols |
apply | Function call proxy(args) | Any value |
construct | new proxy(args) | Object |
Rune AI
Key Insights
- Proxy provides 13 traps that map to every internal object operation from property access to prototype manipulation: Each trap receives the target object and operation parameters, enabling complete interception
- Handler invariants prevent proxies from violating non-configurable property constraints, enforced by the engine: The JavaScript specification requires traps to be consistent with the target's actual property descriptors
- Revocable proxies can be permanently disabled to cut off access and enable garbage collection of the target: Use Proxy.revocable() for session management, temporary grants, and security boundaries
- Proxy composition through chaining enables layered behaviors like logging, caching, and defaults on the same object: Each proxy layer handles one concern, following the single responsibility principle
- Proxy access is 5-50x slower than direct property access because V8 cannot apply inline caching or hidden class optimizations: Use proxies at boundaries, not in hot loops
Frequently Asked Questions
Do Proxy traps affect performance?
What are handler invariants?
Can I proxy built-in objects like Array or Map?
How do revocable proxies help with memory management?
Conclusion
JavaScript Proxy provides 13 traps that intercept every fundamental object operation. Use proxies for validation, access control, lazy initialization, and virtual properties at API boundaries. Combine proxies through composition for layered behavior. For integrating Proxy with the Reflect API, see Using Reflect and Proxy Together in JavaScript. For data binding patterns built on proxies, explore Data Binding with JS Proxies Complete Guide.
More in this topic
OffscreenCanvas API in JS for UI Performance
Master the OffscreenCanvas API to offload rendering from the main thread. Covers worker-based 2D and WebGL rendering, animation loops inside workers, bitmap transfer, double buffering, chart rendering pipelines, image processing, and performance measurement strategies.
Advanced Web Workers for High Performance JS
Master Web Workers for truly parallel JavaScript execution. Covers dedicated and shared workers, structured cloning, transferable objects, SharedArrayBuffer with Atomics, worker pools, task scheduling, Comlink RPC patterns, module workers, and performance profiling strategies.
JavaScript Macros and Abstract Code Generation
Master JavaScript code generation techniques for compile-time and runtime metaprogramming. Covers AST manipulation, Babel plugin authorship, tagged template literals as macros, code generation pipelines, source-to-source transformation, compile-time evaluation, and safe eval alternatives.