Back to Blog
JavaScriptProxyReflectMetaprogramming

JavaScript Proxy and Reflect API

Master JavaScript Proxy and Reflect. From basic traps to reactive programming to validation patterns.

B
Bootspring Team
Engineering
July 16, 2020
7 min read

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 object

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

Share this article

Help spread the word about Bootspring