Proxy intercepts object operations. Reflect provides methods for those operations. Here's how to use them.
Basic Proxy#
1const target = {
2 name: 'John',
3 age: 30,
4};
5
6const handler = {
7 get(target, property, receiver) {
8 console.log(`Getting ${property}`);
9 return target[property];
10 },
11 set(target, property, value, receiver) {
12 console.log(`Setting ${property} to ${value}`);
13 target[property] = value;
14 return true;
15 },
16};
17
18const proxy = new Proxy(target, handler);
19
20proxy.name; // Logs: Getting name, Returns: 'John'
21proxy.age = 31; // Logs: Setting age to 31Validation#
1const validator = {
2 set(target, property, value) {
3 if (property === 'age') {
4 if (typeof value !== 'number') {
5 throw new TypeError('Age must be a number');
6 }
7 if (value < 0 || value > 150) {
8 throw new RangeError('Age must be between 0 and 150');
9 }
10 }
11
12 if (property === 'email') {
13 if (!value.includes('@')) {
14 throw new Error('Invalid email address');
15 }
16 }
17
18 target[property] = value;
19 return true;
20 },
21};
22
23const user = new Proxy({}, validator);
24
25user.age = 25; // OK
26// user.age = 'old'; // TypeError: Age must be a number
27// user.age = 200; // RangeError: Age must be between 0 and 150
28
29user.email = 'john@example.com'; // OK
30// user.email = 'invalid'; // Error: Invalid email addressProperty Access Control#
1// Hide private properties
2const privateHandler = {
3 get(target, property) {
4 if (property.startsWith('_')) {
5 throw new Error(`Cannot access private property: ${property}`);
6 }
7 return target[property];
8 },
9 set(target, property, value) {
10 if (property.startsWith('_')) {
11 throw new Error(`Cannot set private property: ${property}`);
12 }
13 target[property] = value;
14 return true;
15 },
16 has(target, property) {
17 if (property.startsWith('_')) {
18 return false;
19 }
20 return property in target;
21 },
22 ownKeys(target) {
23 return Object.keys(target).filter(key => !key.startsWith('_'));
24 },
25};
26
27const obj = new Proxy(
28 { name: 'John', _secret: 'hidden' },
29 privateHandler
30);
31
32console.log(obj.name); // 'John'
33// console.log(obj._secret); // Error
34console.log('_secret' in obj); // false
35console.log(Object.keys(obj)); // ['name']Default Values#
1const withDefaults = (target, defaults) => {
2 return new Proxy(target, {
3 get(target, property) {
4 if (property in target) {
5 return target[property];
6 }
7 return defaults[property];
8 },
9 });
10};
11
12const config = withDefaults(
13 { port: 3000 },
14 { host: 'localhost', port: 8080, debug: false }
15);
16
17console.log(config.port); // 3000 (from target)
18console.log(config.host); // 'localhost' (from defaults)
19console.log(config.debug); // false (from defaults)Reactive System#
1// Simple reactivity like Vue
2function reactive(target) {
3 const subscribers = new Map();
4
5 return new Proxy(target, {
6 get(target, property) {
7 // Track dependency
8 track(target, property);
9 return target[property];
10 },
11 set(target, property, value) {
12 const oldValue = target[property];
13 target[property] = value;
14
15 // Trigger updates
16 if (oldValue !== value) {
17 trigger(target, property);
18 }
19 return true;
20 },
21 });
22}
23
24let activeEffect = null;
25
26function effect(fn) {
27 activeEffect = fn;
28 fn();
29 activeEffect = null;
30}
31
32const targetMap = new WeakMap();
33
34function track(target, property) {
35 if (!activeEffect) return;
36
37 let depsMap = targetMap.get(target);
38 if (!depsMap) {
39 targetMap.set(target, (depsMap = new Map()));
40 }
41
42 let deps = depsMap.get(property);
43 if (!deps) {
44 depsMap.set(property, (deps = new Set()));
45 }
46
47 deps.add(activeEffect);
48}
49
50function trigger(target, property) {
51 const depsMap = targetMap.get(target);
52 if (!depsMap) return;
53
54 const deps = depsMap.get(property);
55 if (deps) {
56 deps.forEach(effect => effect());
57 }
58}
59
60// Usage
61const state = reactive({ count: 0 });
62
63effect(() => {
64 console.log('Count:', state.count);
65});
66
67state.count++; // Logs: Count: 1
68state.count++; // Logs: Count: 2Logging and Debugging#
1function createLoggingProxy(target, name = 'Object') {
2 return new Proxy(target, {
3 get(target, property, receiver) {
4 console.log(`[GET] ${name}.${String(property)}`);
5 const value = Reflect.get(target, property, receiver);
6 return typeof value === 'object' && value !== null
7 ? createLoggingProxy(value, `${name}.${String(property)}`)
8 : value;
9 },
10 set(target, property, value, receiver) {
11 console.log(`[SET] ${name}.${String(property)} =`, value);
12 return Reflect.set(target, property, value, receiver);
13 },
14 deleteProperty(target, property) {
15 console.log(`[DELETE] ${name}.${String(property)}`);
16 return Reflect.deleteProperty(target, property);
17 },
18 apply(target, thisArg, args) {
19 console.log(`[CALL] ${name}(`, args, ')');
20 return Reflect.apply(target, thisArg, args);
21 },
22 });
23}
24
25const user = createLoggingProxy({
26 name: 'John',
27 address: { city: 'NYC' },
28});
29
30user.name; // [GET] Object.name
31user.address.city; // [GET] Object.address, [GET] Object.address.city
32user.age = 30; // [SET] Object.age = 30Function Proxy#
1// Memoization
2function memoize(fn) {
3 const cache = new Map();
4
5 return new Proxy(fn, {
6 apply(target, thisArg, args) {
7 const key = JSON.stringify(args);
8
9 if (cache.has(key)) {
10 console.log('Cache hit');
11 return cache.get(key);
12 }
13
14 const result = Reflect.apply(target, thisArg, args);
15 cache.set(key, result);
16 return result;
17 },
18 });
19}
20
21const expensiveFn = memoize((n) => {
22 console.log('Computing...');
23 return n * 2;
24});
25
26expensiveFn(5); // Computing... 10
27expensiveFn(5); // Cache hit 10
28
29// Rate limiting
30function rateLimit(fn, limit, interval) {
31 let calls = 0;
32 let resetTime = Date.now() + interval;
33
34 return new Proxy(fn, {
35 apply(target, thisArg, args) {
36 const now = Date.now();
37
38 if (now > resetTime) {
39 calls = 0;
40 resetTime = now + interval;
41 }
42
43 if (calls >= limit) {
44 throw new Error('Rate limit exceeded');
45 }
46
47 calls++;
48 return Reflect.apply(target, thisArg, args);
49 },
50 });
51}
52
53const limitedFn = rateLimit(console.log, 3, 1000);Reflect API#
1// Reflect methods mirror Proxy traps
2const obj = { a: 1, b: 2 };
3
4// Get
5Reflect.get(obj, 'a'); // 1
6
7// Set
8Reflect.set(obj, 'c', 3); // true
9
10// Has
11Reflect.has(obj, 'a'); // true
12
13// Delete
14Reflect.deleteProperty(obj, 'a'); // true
15
16// Keys
17Reflect.ownKeys(obj); // ['b', 'c']
18
19// Define property
20Reflect.defineProperty(obj, 'd', { value: 4 });
21
22// Get prototype
23Reflect.getPrototypeOf(obj); // Object.prototype
24
25// Set prototype
26Reflect.setPrototypeOf(obj, null);
27
28// Is extensible
29Reflect.isExtensible(obj); // true
30
31// Prevent extensions
32Reflect.preventExtensions(obj);
33
34// Apply function
35function greet(greeting) {
36 return `${greeting}, ${this.name}`;
37}
38Reflect.apply(greet, { name: 'John' }, ['Hello']); // 'Hello, John'
39
40// Construct
41class User {
42 constructor(name) {
43 this.name = name;
44 }
45}
46Reflect.construct(User, ['John']); // User { name: 'John' }Revocable Proxy#
1// Create proxy that can be disabled
2const { proxy, revoke } = Proxy.revocable(
3 { secret: 'data' },
4 {
5 get(target, property) {
6 return target[property];
7 },
8 }
9);
10
11console.log(proxy.secret); // 'data'
12
13revoke();
14
15// console.log(proxy.secret); // TypeError: Cannot perform 'get' on a proxy that has been revoked
16
17// Useful for access control
18function grantAccess(data, expiryMs) {
19 const { proxy, revoke } = Proxy.revocable(data, {});
20
21 setTimeout(revoke, expiryMs);
22
23 return proxy;
24}
25
26const tempAccess = grantAccess({ sensitive: 'info' }, 5000);Array Proxy#
1// Observable array
2function observableArray(array, callback) {
3 return new Proxy(array, {
4 set(target, property, value) {
5 const result = Reflect.set(target, property, value);
6
7 if (property !== 'length') {
8 callback('set', { property, value });
9 }
10
11 return result;
12 },
13 deleteProperty(target, property) {
14 const result = Reflect.deleteProperty(target, property);
15 callback('delete', { property });
16 return result;
17 },
18 });
19}
20
21const arr = observableArray([], (action, data) => {
22 console.log(`Array ${action}:`, data);
23});
24
25arr.push(1); // Array set: { property: '0', value: 1 }
26arr.push(2); // Array set: { property: '1', value: 2 }
27arr[0] = 10; // Array set: { property: '0', value: 10 }Type Coercion#
1// Auto-convert types
2const typedHandler = {
3 set(target, property, value) {
4 const schema = target._schema?.[property];
5
6 if (schema) {
7 switch (schema) {
8 case 'number':
9 value = Number(value);
10 break;
11 case 'string':
12 value = String(value);
13 break;
14 case 'boolean':
15 value = Boolean(value);
16 break;
17 case 'date':
18 value = new Date(value);
19 break;
20 }
21 }
22
23 target[property] = value;
24 return true;
25 },
26};
27
28const record = new Proxy(
29 { _schema: { age: 'number', active: 'boolean', joined: 'date' } },
30 typedHandler
31);
32
33record.age = '25'; // Stored as 25 (number)
34record.active = 1; // Stored as true (boolean)
35record.joined = '2024-01-15'; // Stored as Date objectBest Practices#
Usage:
✓ Use Reflect with Proxy handlers
✓ Return true from set traps
✓ Handle all necessary traps
✓ Consider revocable for access control
Performance:
✓ Avoid proxying hot paths
✓ Cache proxy instances
✓ Use direct access when possible
✓ Profile proxy overhead
Patterns:
✓ Validation and sanitization
✓ Change detection
✓ Access logging
✓ Default values
Conclusion#
Proxy and Reflect enable powerful metaprogramming in JavaScript. Use Proxy for validation, reactivity, logging, and access control. Always use Reflect methods in handlers for correct behavior with inheritance and receivers.