Back to Blog
JavaScriptProxyReflectMetaprogramming

JavaScript Proxy and Reflect API

Master Proxy and Reflect in JavaScript. From validation to reactive systems to debugging tools.

B
Bootspring Team
Engineering
March 21, 2021
7 min read

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 31

Validation#

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 address

Property 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: 2

Logging 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 = 30

Function 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 object

Best 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.

Share this article

Help spread the word about Bootspring