Proxy enables custom behavior for fundamental operations. Here's how to use it effectively.
Basic Proxy#
1// Simple proxy
2const target = { name: 'Alice', age: 25 };
3
4const handler = {
5 get(target, property) {
6 console.log(`Getting ${property}`);
7 return target[property];
8 },
9 set(target, property, value) {
10 console.log(`Setting ${property} to ${value}`);
11 target[property] = value;
12 return true;
13 },
14};
15
16const proxy = new Proxy(target, handler);
17
18proxy.name; // Logs: "Getting name", returns "Alice"
19proxy.age = 26; // Logs: "Setting age to 26"
20
21// With Reflect
22const handlerWithReflect = {
23 get(target, property, receiver) {
24 console.log(`Getting ${property}`);
25 return Reflect.get(target, property, receiver);
26 },
27 set(target, property, value, receiver) {
28 console.log(`Setting ${property} to ${value}`);
29 return Reflect.set(target, property, value, receiver);
30 },
31};Common Traps#
1const handler = {
2 // Property access
3 get(target, property, receiver) {
4 return Reflect.get(target, property, receiver);
5 },
6
7 // Property assignment
8 set(target, property, value, receiver) {
9 return Reflect.set(target, property, value, receiver);
10 },
11
12 // Property check
13 has(target, property) {
14 return Reflect.has(target, property);
15 },
16
17 // Property deletion
18 deleteProperty(target, property) {
19 return Reflect.deleteProperty(target, property);
20 },
21
22 // Object.keys, for...in
23 ownKeys(target) {
24 return Reflect.ownKeys(target);
25 },
26
27 // Object.getOwnPropertyDescriptor
28 getOwnPropertyDescriptor(target, property) {
29 return Reflect.getOwnPropertyDescriptor(target, property);
30 },
31
32 // Object.defineProperty
33 defineProperty(target, property, descriptor) {
34 return Reflect.defineProperty(target, property, descriptor);
35 },
36
37 // Function call
38 apply(target, thisArg, args) {
39 return Reflect.apply(target, thisArg, args);
40 },
41
42 // new operator
43 construct(target, args, newTarget) {
44 return Reflect.construct(target, args, newTarget);
45 },
46};Validation#
1// Property validation
2function createValidatedObject(schema) {
3 return new Proxy({}, {
4 set(target, property, value) {
5 const validator = schema[property];
6
7 if (!validator) {
8 throw new Error(`Unknown property: ${property}`);
9 }
10
11 if (!validator(value)) {
12 throw new Error(`Invalid value for ${property}: ${value}`);
13 }
14
15 target[property] = value;
16 return true;
17 },
18 });
19}
20
21const userSchema = {
22 name: (v) => typeof v === 'string' && v.length > 0,
23 age: (v) => typeof v === 'number' && v >= 0 && v <= 150,
24 email: (v) => typeof v === 'string' && v.includes('@'),
25};
26
27const user = createValidatedObject(userSchema);
28
29user.name = 'Alice'; // OK
30user.age = 25; // OK
31user.email = 'alice@example.com'; // OK
32user.age = -5; // Error: Invalid value for age: -5
33
34// Type-safe proxy
35function createTypedObject(types) {
36 const data = {};
37
38 return new Proxy(data, {
39 set(target, property, value) {
40 const expectedType = types[property];
41
42 if (expectedType && typeof value !== expectedType) {
43 throw new TypeError(
44 `${property} must be ${expectedType}, got ${typeof value}`
45 );
46 }
47
48 target[property] = value;
49 return true;
50 },
51 });
52}
53
54const config = createTypedObject({
55 port: 'number',
56 host: 'string',
57 debug: 'boolean',
58});Reactive Programming#
1// Observable object
2function createReactive(target, callback) {
3 return new Proxy(target, {
4 set(target, property, value, receiver) {
5 const oldValue = target[property];
6 const result = Reflect.set(target, property, value, receiver);
7
8 if (oldValue !== value) {
9 callback(property, value, oldValue);
10 }
11
12 return result;
13 },
14 });
15}
16
17const state = createReactive({ count: 0 }, (prop, newVal, oldVal) => {
18 console.log(`${prop} changed from ${oldVal} to ${newVal}`);
19});
20
21state.count = 1; // Logs: "count changed from 0 to 1"
22
23// Deep reactive
24function deepReactive(target, callback, path = '') {
25 const handler = {
26 get(target, property, receiver) {
27 const value = Reflect.get(target, property, receiver);
28
29 if (typeof value === 'object' && value !== null) {
30 return deepReactive(
31 value,
32 callback,
33 path ? `${path}.${property}` : property
34 );
35 }
36
37 return value;
38 },
39
40 set(target, property, value, receiver) {
41 const fullPath = path ? `${path}.${property}` : property;
42 const oldValue = target[property];
43 const result = Reflect.set(target, property, value, receiver);
44
45 if (oldValue !== value) {
46 callback(fullPath, value, oldValue);
47 }
48
49 return result;
50 },
51 };
52
53 return new Proxy(target, handler);
54}
55
56const deepState = deepReactive(
57 { user: { name: 'Alice', settings: { theme: 'dark' } } },
58 (path, newVal, oldVal) => {
59 console.log(`${path}: ${oldVal} -> ${newVal}`);
60 }
61);
62
63deepState.user.settings.theme = 'light';
64// Logs: "user.settings.theme: dark -> light"Default Values#
1// Default property values
2function withDefaults(target, defaults) {
3 return new Proxy(target, {
4 get(target, property, receiver) {
5 const value = Reflect.get(target, property, receiver);
6 return value !== undefined ? value : defaults[property];
7 },
8 });
9}
10
11const config = withDefaults(
12 { port: 8080 },
13 { port: 3000, host: 'localhost', debug: false }
14);
15
16console.log(config.port); // 8080 (from target)
17console.log(config.host); // 'localhost' (from defaults)
18console.log(config.debug); // false (from defaults)
19
20// Nested defaults
21function withNestedDefaults(target, defaults) {
22 return new Proxy(target, {
23 get(target, property, receiver) {
24 let value = Reflect.get(target, property, receiver);
25 const defaultValue = defaults[property];
26
27 if (value === undefined) {
28 value = defaultValue;
29 } else if (
30 typeof value === 'object' &&
31 typeof defaultValue === 'object'
32 ) {
33 value = withNestedDefaults(value, defaultValue);
34 }
35
36 return value;
37 },
38 });
39}Access Control#
1// Private properties
2function createPrivateObject(target, privateKeys) {
3 return new Proxy(target, {
4 get(target, property, receiver) {
5 if (privateKeys.includes(property)) {
6 throw new Error(`Cannot access private property: ${property}`);
7 }
8 return Reflect.get(target, property, receiver);
9 },
10
11 set(target, property, value, receiver) {
12 if (privateKeys.includes(property)) {
13 throw new Error(`Cannot set private property: ${property}`);
14 }
15 return Reflect.set(target, property, value, receiver);
16 },
17
18 ownKeys(target) {
19 return Reflect.ownKeys(target).filter(
20 (key) => !privateKeys.includes(key)
21 );
22 },
23 });
24}
25
26const user = createPrivateObject(
27 { name: 'Alice', password: 'secret123' },
28 ['password']
29);
30
31console.log(user.name); // 'Alice'
32console.log(user.password); // Error: Cannot access private property
33
34// Read-only object
35function readonly(target) {
36 return new Proxy(target, {
37 set() {
38 throw new Error('Cannot modify readonly object');
39 },
40 deleteProperty() {
41 throw new Error('Cannot delete from readonly object');
42 },
43 });
44}
45
46const frozenConfig = readonly({ apiKey: '12345' });
47frozenConfig.apiKey = 'new'; // Error: Cannot modify readonly objectFunction Proxies#
1// Logging decorator
2function createLogger(fn, name) {
3 return new Proxy(fn, {
4 apply(target, thisArg, args) {
5 console.log(`Calling ${name} with`, args);
6 const result = Reflect.apply(target, thisArg, args);
7 console.log(`${name} returned`, result);
8 return result;
9 },
10 });
11}
12
13const add = createLogger((a, b) => a + b, 'add');
14add(2, 3); // Logs: Calling add with [2, 3], add returned 5
15
16// Memoization
17function memoize(fn) {
18 const cache = new Map();
19
20 return new Proxy(fn, {
21 apply(target, thisArg, args) {
22 const key = JSON.stringify(args);
23
24 if (cache.has(key)) {
25 return cache.get(key);
26 }
27
28 const result = Reflect.apply(target, thisArg, args);
29 cache.set(key, result);
30 return result;
31 },
32 });
33}
34
35const fibonacci = memoize((n) => {
36 if (n <= 1) return n;
37 return fibonacci(n - 1) + fibonacci(n - 2);
38});
39
40// Rate limiting
41function rateLimit(fn, limit, window) {
42 const calls = [];
43
44 return new Proxy(fn, {
45 apply(target, thisArg, args) {
46 const now = Date.now();
47 const windowStart = now - window;
48
49 // Remove old calls
50 while (calls.length && calls[0] < windowStart) {
51 calls.shift();
52 }
53
54 if (calls.length >= limit) {
55 throw new Error('Rate limit exceeded');
56 }
57
58 calls.push(now);
59 return Reflect.apply(target, thisArg, args);
60 },
61 });
62}Array Proxy#
1// Negative index support
2function createArrayProxy(arr) {
3 return new Proxy(arr, {
4 get(target, property, receiver) {
5 const index = Number(property);
6
7 if (!isNaN(index) && index < 0) {
8 return target[target.length + index];
9 }
10
11 return Reflect.get(target, property, receiver);
12 },
13 });
14}
15
16const arr = createArrayProxy([1, 2, 3, 4, 5]);
17console.log(arr[-1]); // 5
18console.log(arr[-2]); // 4
19
20// Observable array
21function createObservableArray(arr, onChange) {
22 return new Proxy(arr, {
23 set(target, property, value, receiver) {
24 const result = Reflect.set(target, property, value, receiver);
25 onChange(property, value);
26 return result;
27 },
28
29 deleteProperty(target, property) {
30 const result = Reflect.deleteProperty(target, property);
31 onChange(property, undefined);
32 return result;
33 },
34 });
35}Reflect API#
1// Reflect provides default behavior
2const obj = { x: 1 };
3
4// These are equivalent
5Reflect.get(obj, 'x'); // 1
6obj.x; // 1
7
8// But Reflect returns success/failure
9Reflect.set(obj, 'y', 2); // true
10Reflect.defineProperty(obj, 'z', { value: 3 }); // true
11
12// Object methods vs Reflect
13Object.defineProperty(obj, 'a', { value: 1 }); // throws on failure
14Reflect.defineProperty(obj, 'a', { value: 1 }); // returns false on failure
15
16// Method application
17function greet(greeting) {
18 return `${greeting}, ${this.name}`;
19}
20
21Reflect.apply(greet, { name: 'Alice' }, ['Hello']); // "Hello, Alice"
22
23// Constructor
24class Person {
25 constructor(name) {
26 this.name = name;
27 }
28}
29
30const person = Reflect.construct(Person, ['Bob']);
31console.log(person.name); // "Bob"Best Practices#
Design:
✓ Use Reflect for default behavior
✓ Always return from set trap
✓ Preserve prototype chain
✓ Handle all edge cases
Performance:
✓ Avoid proxying hot paths
✓ Cache proxy instances
✓ Minimize trap operations
✓ Profile performance impact
Patterns:
✓ Validation and type checking
✓ Change detection/reactivity
✓ Access control
✓ Logging and debugging
Avoid:
✗ Overusing proxies
✗ Forgetting receiver argument
✗ Breaking invariants
✗ Circular proxy chains
Conclusion#
Proxy and Reflect enable powerful metaprogramming in JavaScript. Use them for validation, reactivity, access control, and function decoration. Always use Reflect for default behavior and be mindful of performance implications.