Advanced JavaScript (ES6+)

Proxy and Reflect API

13 min Lesson 30 of 40

Proxy and Reflect API

The Proxy and Reflect APIs are powerful ES6 features that allow you to intercept and customize fundamental operations on objects. Proxies enable meta-programming, validation, logging, and much more. Let's explore how to use these advanced features.

What is a Proxy?

A Proxy wraps an object and intercepts operations performed on it. You can customize behavior for fundamental operations like property access, assignment, enumeration, function invocation, and more.

Key Concept: A Proxy acts as a middleman between you and the target object, allowing you to intercept and redefine operations.

Creating a Basic Proxy

Create a proxy with a target object and a handler containing traps:

// Target object const target = { name: "John", age: 30 }; // Handler with traps const handler = { // Intercept property access (get trap) get(target, property) { console.log(`Getting property: ${property}`); return target[property]; }, // Intercept property assignment (set trap) set(target, property, value) { console.log(`Setting property: ${property} = ${value}`); target[property] = value; return true; // Indicates success } }; // Create the proxy const proxy = new Proxy(target, handler); // Use the proxy console.log(proxy.name); // Logs: "Getting property: name", then "John" proxy.age = 31; // Logs: "Setting property: age = 31" console.log(proxy.age); // Logs: "Getting property: age", then 31

Common Proxy Traps

Proxies support many traps for different operations:

Common Traps: get(target, property, receiver): - Intercepts property access - Example: obj.prop or obj["prop"] set(target, property, value, receiver): - Intercepts property assignment - Example: obj.prop = value has(target, property): - Intercepts the "in" operator - Example: "prop" in obj deleteProperty(target, property): - Intercepts property deletion - Example: delete obj.prop apply(target, thisArg, argumentsList): - Intercepts function calls - Example: func(...args) construct(target, argumentsList, newTarget): - Intercepts "new" operator - Example: new Func(...args)

Validation with Proxies

Use proxies to validate data before setting properties:

const validator = { set(target, property, value) { if (property === "age") { if (typeof value !== "number") { throw new TypeError("Age must be a number"); } if (value < 0 || value > 150) { throw new RangeError("Age must be between 0 and 150"); } } if (property === "email") { if (!value.includes("@")) { throw new Error("Invalid email format"); } } target[property] = value; return true; } }; const person = new Proxy({}, validator); person.age = 30; // Works console.log(person.age); // 30 person.email = "john@example.com"; // Works console.log(person.email); // "john@example.com" // These will throw errors: // person.age = "thirty"; // TypeError: Age must be a number // person.age = -5; // RangeError: Age must be between 0 and 150 // person.email = "invalid"; // Error: Invalid email format
Use Case: Proxies are excellent for runtime type checking and data validation without cluttering your code with manual checks.

Default Values with Proxies

Return default values for non-existent properties:

const withDefaults = (target, defaultValue) => { return new Proxy(target, { get(target, property) { if (property in target) { return target[property]; } return defaultValue; } }); }; const config = withDefaults({ port: 3000, host: "localhost" }, "Not configured"); console.log(config.port); // 3000 console.log(config.host); // "localhost" console.log(config.database); // "Not configured" console.log(config.anything); // "Not configured"

Logging and Debugging with Proxies

Track all operations on an object for debugging:

const createLogger = (target, name) => { return new Proxy(target, { get(target, property) { console.log(`[${name}] GET: ${property}`); return target[property]; }, set(target, property, value) { console.log(`[${name}] SET: ${property} = ${JSON.stringify(value)}`); target[property] = value; return true; }, deleteProperty(target, property) { console.log(`[${name}] DELETE: ${property}`); delete target[property]; return true; } }); }; const user = createLogger({ name: "John", age: 30 }, "User"); user.name; // [User] GET: name user.age = 31; // [User] SET: age = 31 user.email = "john@example.com"; // [User] SET: email = "john@example.com" delete user.age; // [User] DELETE: age

Virtual Properties with Proxies

Create computed properties that don't actually exist on the object:

const createVirtualProps = (target) => { return new Proxy(target, { get(target, property) { // Real properties if (property in target) { return target[property]; } // Virtual property: fullName if (property === "fullName") { return `${target.firstName} ${target.lastName}`; } // Virtual property: initials if (property === "initials") { return `${target.firstName[0]}.${target.lastName[0]}.`; } // Virtual property: age (calculated from birthYear) if (property === "age") { const currentYear = new Date().getFullYear(); return currentYear - target.birthYear; } return undefined; } }); }; const person = createVirtualProps({ firstName: "John", lastName: "Doe", birthYear: 1990 }); console.log(person.firstName); // "John" console.log(person.fullName); // "John Doe" (virtual) console.log(person.initials); // "J.D." (virtual) console.log(person.age); // 36 (virtual, calculated)

Function Interceptors with Proxies

Intercept function calls and modify behavior:

const createTrackedFunction = (func, name) => { let callCount = 0; return new Proxy(func, { apply(target, thisArg, args) { callCount++; console.log(`[${name}] Call #${callCount} with args:`, args); const startTime = Date.now(); const result = target.apply(thisArg, args); const duration = Date.now() - startTime; console.log(`[${name}] Completed in ${duration}ms, result:`, result); return result; } }); }; const add = (a, b) => a + b; const trackedAdd = createTrackedFunction(add, "add"); trackedAdd(5, 3); // [add] Call #1 with args: [5, 3] // [add] Completed in 0ms, result: 8 trackedAdd(10, 20); // [add] Call #2 with args: [10, 20] // [add] Completed in 0ms, result: 30

The Reflect API

Reflect provides methods for interceptable JavaScript operations. It's often used with Proxies:

// Reflect methods mirror Proxy traps const target = { name: "John", age: 30 }; // Reflect.get() - get property value console.log(Reflect.get(target, "name")); // "John" // Reflect.set() - set property value Reflect.set(target, "age", 31); console.log(target.age); // 31 // Reflect.has() - check if property exists console.log(Reflect.has(target, "name")); // true console.log(Reflect.has(target, "email")); // false // Reflect.deleteProperty() - delete property Reflect.deleteProperty(target, "age"); console.log(target.age); // undefined // Reflect.ownKeys() - get all keys const obj = { a: 1, b: 2 }; console.log(Reflect.ownKeys(obj)); // ["a", "b"] // Reflect.apply() - call function function greet(greeting) { return `${greeting}, ${this.name}!`; } console.log(Reflect.apply(greet, { name: "John" }, ["Hello"])); // "Hello, John!"
Why Reflect? Reflect provides a cleaner, more consistent API for meta-programming operations. It returns success/failure as boolean rather than throwing errors.

Using Reflect in Proxy Traps

Reflect is commonly used within proxy traps to forward operations:

const handler = { get(target, property, receiver) { console.log(`Accessing property: ${property}`); // Use Reflect to forward the operation return Reflect.get(target, property, receiver); }, set(target, property, value, receiver) { console.log(`Setting ${property} to ${value}`); // Use Reflect to forward the operation return Reflect.set(target, property, value, receiver); }, has(target, property) { console.log(`Checking if ${property} exists`); return Reflect.has(target, property); } }; const obj = new Proxy({ name: "John", age: 30 }, handler); console.log(obj.name); // Accessing property: name, then "John" obj.age = 31; // Setting age to 31 console.log("name" in obj); // Checking if name exists, then true

Negative Array Indices with Proxies

Implement Python-style negative indexing for arrays:

const createNegativeArray = (array) => { return new Proxy(array, { get(target, property) { const index = Number(property); // Handle negative indices if (index < 0) { return target[target.length + index]; } return Reflect.get(target, property); } }); }; const arr = createNegativeArray(["a", "b", "c", "d", "e"]); console.log(arr[0]); // "a" console.log(arr[-1]); // "e" (last element) console.log(arr[-2]); // "d" (second to last) console.log(arr[-5]); // "a" (first element via negative index)

Read-Only Objects with Proxies

Create truly immutable objects:

const createReadOnly = (target) => { return new Proxy(target, { set(target, property, value) { throw new Error(`Cannot modify read-only property: ${property}`); }, deleteProperty(target, property) { throw new Error(`Cannot delete read-only property: ${property}`); }, defineProperty(target, property, descriptor) { throw new Error(`Cannot define property on read-only object: ${property}`); } }); }; const config = createReadOnly({ apiUrl: "https://api.example.com", timeout: 5000 }); console.log(config.apiUrl); // "https://api.example.com" // These all throw errors: // config.apiUrl = "new-url"; // Error: Cannot modify read-only property: apiUrl // delete config.timeout; // Error: Cannot delete read-only property: timeout // config.newProp = "value"; // Error: Cannot modify read-only property: newProp

Observable Pattern with Proxies

Implement reactive programming patterns:

const createObservable = (target, callback) => { return new Proxy(target, { set(target, property, value) { const oldValue = target[property]; target[property] = value; // Notify observers of the change callback(property, oldValue, value); return true; } }); }; const state = createObservable( { count: 0, name: "App" }, (property, oldValue, newValue) => { console.log(`Property "${property}" changed from ${oldValue} to ${newValue}`); } ); state.count = 1; // Property "count" changed from 0 to 1 state.count = 2; // Property "count" changed from 1 to 2 state.name = "MyApp"; // Property "name" changed from App to MyApp
Real-World Use: This pattern is the foundation of reactive frameworks like Vue.js, which use Proxies for reactive data binding.

Practice Exercise:

Challenge: Create a smart cache system using Proxy with the following features:

  • Track cache hits and misses
  • Automatically compute and cache expensive operations
  • Provide cache statistics (hits, misses, hit rate)
  • Allow cache clearing

Solution:

const createSmartCache = (computeFunction) => { const cache = new Map(); let hits = 0; let misses = 0; const handler = { get(target, property) { // Special methods if (property === "stats") { return () => ({ hits, misses, total: hits + misses, hitRate: hits / (hits + misses) || 0, cacheSize: cache.size }); } if (property === "clear") { return () => { cache.clear(); hits = 0; misses = 0; }; } // Check cache if (cache.has(property)) { hits++; console.log(`Cache HIT for: ${property}`); return cache.get(property); } // Cache miss - compute value misses++; console.log(`Cache MISS for: ${property}`); const value = computeFunction(property); cache.set(property, value); return value; } }; return new Proxy({}, handler); }; // Example: Expensive fibonacci computation const fibonacci = (n) => { if (n <= 1) return n; return fibonacci(n - 1) + fibonacci(n - 2); }; const smartFib = createSmartCache((n) => fibonacci(Number(n))); console.log(smartFib[10]); // Cache MISS for: 10, computes value console.log(smartFib[10]); // Cache HIT for: 10, returns cached console.log(smartFib[5]); // Cache MISS for: 5, computes value console.log(smartFib[10]); // Cache HIT for: 10, returns cached console.log(smartFib.stats()); // { hits: 2, misses: 2, total: 4, hitRate: 0.5, cacheSize: 2 } smartFib.clear(); console.log(smartFib.stats()); // { hits: 0, misses: 0, total: 0, hitRate: 0, cacheSize: 0 }

Performance Considerations

Proxies add overhead to operations. Use them judiciously:

// Proxies have performance cost const plainObject = { x: 1, y: 2 }; const proxiedObject = new Proxy({ x: 1, y: 2 }, { get(target, property) { return target[property]; } }); // Benchmark console.time("Plain object"); for (let i = 0; i < 1000000; i++) { plainObject.x; } console.timeEnd("Plain object"); // ~3ms console.time("Proxied object"); for (let i = 0; i < 1000000; i++) { proxiedObject.x; } console.timeEnd("Proxied object"); // ~20ms (slower)
Performance Tip: Proxies are powerful but add overhead. Use them for developer tools, validation, and complex scenarios - not for hot paths in performance-critical code.

Summary

In this lesson, you learned:

  • Proxies intercept and customize object operations
  • Common traps: get, set, has, deleteProperty, apply, construct
  • Use cases: validation, logging, virtual properties, observables
  • Reflect API provides methods for forwarding operations
  • Proxies enable meta-programming and reactive patterns
  • Proxies have performance overhead - use appropriately
  • Real-world applications in frameworks and tools
Congratulations! You've completed Module 5: Object-Oriented JavaScript! You now understand ES6 classes, inheritance, prototypes, object methods, and advanced proxy patterns. In the next module, we'll explore ES6 modules and code organization!