Proxy allows you to intercept and customize fundamental operations on objects. 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) {
8 console.log(`Getting ${property}`);
9 return target[property];
10 },
11 set(target, property, value) {
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 31"Validation with Proxy#
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 target[property] = value;
12 return true;
13 },
14};
15
16const person = new Proxy({}, validator);
17
18person.age = 30; // OK
19person.age = -5; // RangeError
20person.age = 'thirty'; // TypeErrorDefault Values#
1const withDefaults = (target, defaults) => {
2 return new Proxy(target, {
3 get(obj, prop) {
4 return prop in obj ? obj[prop] : defaults[prop];
5 },
6 });
7};
8
9const config = withDefaults(
10 { host: 'localhost' },
11 { host: '0.0.0.0', port: 3000, timeout: 5000 }
12);
13
14console.log(config.host); // 'localhost' (from target)
15console.log(config.port); // 3000 (from defaults)
16console.log(config.timeout); // 5000 (from defaults)Property Access Logging#
1function createLogger(obj, name = 'Object') {
2 return new Proxy(obj, {
3 get(target, prop) {
4 console.log(`[${name}] GET ${String(prop)}`);
5 return Reflect.get(target, prop);
6 },
7 set(target, prop, value) {
8 console.log(`[${name}] SET ${String(prop)} = ${value}`);
9 return Reflect.set(target, prop, value);
10 },
11 deleteProperty(target, prop) {
12 console.log(`[${name}] DELETE ${String(prop)}`);
13 return Reflect.deleteProperty(target, prop);
14 },
15 });
16}
17
18const user = createLogger({ name: 'John' }, 'User');
19user.name; // [User] GET name
20user.age = 30; // [User] SET age = 30
21delete user.age; // [User] DELETE ageReflect API#
1// Reflect provides methods matching Proxy traps
2const obj = { x: 1, y: 2 };
3
4// Get property
5Reflect.get(obj, 'x'); // 1
6
7// Set property
8Reflect.set(obj, 'z', 3); // true
9
10// Check property
11Reflect.has(obj, 'x'); // true
12
13// Delete property
14Reflect.deleteProperty(obj, 'z'); // true
15
16// Get keys
17Reflect.ownKeys(obj); // ['x', 'y']
18
19// Define property
20Reflect.defineProperty(obj, 'computed', {
21 get() {
22 return this.x + this.y;
23 },
24});
25
26// Get prototype
27Reflect.getPrototypeOf(obj); // Object.prototype
28
29// Set prototype
30Reflect.setPrototypeOf(obj, null);Revocable Proxy#
1const target = { secret: 'classified' };
2
3const { proxy, revoke } = Proxy.revocable(target, {
4 get(target, prop) {
5 return target[prop];
6 },
7});
8
9console.log(proxy.secret); // 'classified'
10
11// Revoke access
12revoke();
13
14console.log(proxy.secret); // TypeError: Cannot perform 'get' on a proxy that has been revokedObservable Pattern#
1function observable(target) {
2 const listeners = new Map();
3
4 return new Proxy(target, {
5 set(obj, prop, value) {
6 const oldValue = obj[prop];
7 obj[prop] = value;
8
9 if (listeners.has(prop)) {
10 listeners.get(prop).forEach((callback) => {
11 callback(value, oldValue, prop);
12 });
13 }
14
15 return true;
16 },
17
18 get(obj, prop) {
19 if (prop === 'subscribe') {
20 return (property, callback) => {
21 if (!listeners.has(property)) {
22 listeners.set(property, new Set());
23 }
24 listeners.get(property).add(callback);
25
26 // Return unsubscribe function
27 return () => listeners.get(property).delete(callback);
28 };
29 }
30 return obj[prop];
31 },
32 });
33}
34
35const state = observable({ count: 0 });
36
37const unsubscribe = state.subscribe('count', (newVal, oldVal) => {
38 console.log(`Count changed from ${oldVal} to ${newVal}`);
39});
40
41state.count = 1; // "Count changed from 0 to 1"
42state.count = 2; // "Count changed from 1 to 2"
43
44unsubscribe();
45state.count = 3; // No logNegative Array Indices#
1function negativeArray(arr) {
2 return new Proxy(arr, {
3 get(target, prop) {
4 const index = Number(prop);
5 if (!isNaN(index) && index < 0) {
6 return target[target.length + index];
7 }
8 return Reflect.get(target, prop);
9 },
10 set(target, prop, value) {
11 const index = Number(prop);
12 if (!isNaN(index) && index < 0) {
13 target[target.length + index] = value;
14 return true;
15 }
16 return Reflect.set(target, prop, value);
17 },
18 });
19}
20
21const arr = negativeArray([1, 2, 3, 4, 5]);
22console.log(arr[-1]); // 5
23console.log(arr[-2]); // 4
24arr[-1] = 10;
25console.log(arr); // [1, 2, 3, 4, 10]Private Properties#
1function privateProps(target, privateKeys = []) {
2 return new Proxy(target, {
3 get(obj, prop) {
4 if (privateKeys.includes(prop)) {
5 throw new Error(`Cannot access private property: ${prop}`);
6 }
7 return Reflect.get(obj, prop);
8 },
9 set(obj, prop, value) {
10 if (privateKeys.includes(prop)) {
11 throw new Error(`Cannot set private property: ${prop}`);
12 }
13 return Reflect.set(obj, prop, value);
14 },
15 has(obj, prop) {
16 if (privateKeys.includes(prop)) {
17 return false;
18 }
19 return Reflect.has(obj, prop);
20 },
21 ownKeys(obj) {
22 return Reflect.ownKeys(obj).filter((key) => !privateKeys.includes(key));
23 },
24 });
25}
26
27const user = privateProps(
28 { name: 'John', _password: 'secret123' },
29 ['_password']
30);
31
32console.log(user.name); // 'John'
33console.log(user._password); // Error
34console.log('_password' in user); // false
35console.log(Object.keys(user)); // ['name']Function Proxy#
1function tracedFunction(fn, name = fn.name) {
2 return new Proxy(fn, {
3 apply(target, thisArg, args) {
4 console.log(`Calling ${name} with:`, args);
5 const start = performance.now();
6 const result = Reflect.apply(target, thisArg, args);
7 const duration = performance.now() - start;
8 console.log(`${name} returned:`, result, `(${duration.toFixed(2)}ms)`);
9 return result;
10 },
11 });
12}
13
14const add = tracedFunction((a, b) => a + b, 'add');
15add(2, 3);
16// Calling add with: [2, 3]
17// add returned: 5 (0.01ms)Caching Proxy#
1function memoize(fn) {
2 const cache = new Map();
3
4 return new Proxy(fn, {
5 apply(target, thisArg, args) {
6 const key = JSON.stringify(args);
7
8 if (cache.has(key)) {
9 console.log('Cache hit');
10 return cache.get(key);
11 }
12
13 console.log('Cache miss');
14 const result = Reflect.apply(target, thisArg, args);
15 cache.set(key, result);
16 return result;
17 },
18 });
19}
20
21const expensiveCalc = memoize((n) => {
22 // Simulate expensive operation
23 let result = 0;
24 for (let i = 0; i < n * 1000000; i++) {
25 result += i;
26 }
27 return result;
28});
29
30expensiveCalc(10); // Cache miss
31expensiveCalc(10); // Cache hitDOM Element Proxy#
1function $(selector) {
2 const elements = document.querySelectorAll(selector);
3
4 return new Proxy(elements, {
5 get(target, prop) {
6 // Array methods
7 if (prop in Array.prototype) {
8 return Array.prototype[prop].bind([...target]);
9 }
10
11 // Style shorthand
12 if (prop === 'css') {
13 return (styles) => {
14 target.forEach((el) => Object.assign(el.style, styles));
15 return proxy;
16 };
17 }
18
19 // Event shorthand
20 if (prop.startsWith('on')) {
21 const event = prop.slice(2).toLowerCase();
22 return (handler) => {
23 target.forEach((el) => el.addEventListener(event, handler));
24 return proxy;
25 };
26 }
27
28 // Return first element's property
29 return target[0]?.[prop];
30 },
31 });
32
33 const proxy = new Proxy(elements, handler);
34 return proxy;
35}
36
37// Usage
38$('.button').css({ color: 'red' }).onClick((e) => console.log(e));Type Coercion#
1const coerce = (schema) => (target) => {
2 return new Proxy(target, {
3 set(obj, prop, value) {
4 if (prop in schema) {
5 const type = schema[prop];
6 let coerced = value;
7
8 switch (type) {
9 case 'number':
10 coerced = Number(value);
11 break;
12 case 'string':
13 coerced = String(value);
14 break;
15 case 'boolean':
16 coerced = Boolean(value);
17 break;
18 case 'date':
19 coerced = new Date(value);
20 break;
21 }
22
23 return Reflect.set(obj, prop, coerced);
24 }
25 return Reflect.set(obj, prop, value);
26 },
27 });
28};
29
30const form = coerce({
31 age: 'number',
32 name: 'string',
33 active: 'boolean',
34})({});
35
36form.age = '25';
37form.active = 1;
38console.log(form.age); // 25 (number)
39console.log(form.active); // true (boolean)Best Practices#
Usage:
✓ Validation and constraints
✓ Logging and debugging
✓ Caching and memoization
✓ Observable state
Reflect:
✓ Use with Proxy traps
✓ Forward default behavior
✓ Handle edge cases
✓ Return proper values
Performance:
✓ Cache proxy instances
✓ Minimize trap overhead
✓ Use for specific needs
✓ Profile impact
Avoid:
✗ Proxying everything
✗ Complex nested proxies
✗ Ignoring invariants
✗ Memory leak risks
Conclusion#
Proxy and Reflect provide powerful metaprogramming capabilities in JavaScript. Use Proxy for validation, logging, caching, and reactive state management. Use Reflect to implement default behavior in proxy traps. Be mindful of performance and only use proxies when their benefits outweigh the overhead.