Back to Blog
JavaScriptProxyReflectMetaprogramming

JavaScript Proxy and Reflect Guide

Master JavaScript Proxy and Reflect for metaprogramming and object interception.

B
Bootspring Team
Engineering
January 3, 2019
7 min read

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'; // TypeError

Default 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 age

Reflect 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 revoked

Observable 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 log

Negative 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 hit

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

Share this article

Help spread the word about Bootspring