Proxy Pattern
HardThe Proxy Pattern provides a surrogate or placeholder for another object, controlling access to it. In JavaScript, the built-in Proxy object makes this pattern first-class, enabling interception of fundamental operations like property access, assignment, function invocation, and enumeration.
Interactive Visualization
Creational Patterns
Understanding Proxy Pattern
The Proxy Pattern places an intermediary between the client and the real object, intercepting and potentially modifying operations. JavaScript is unique among mainstream languages in having a native Proxy constructor that can intercept virtually every object operation: property reads, writes, deletion, enumeration, function calls, and even the in operator.
The handler object defines trap functions that intercept these operations. Inside each trap, the Reflect API provides the default behavior, so you can selectively override only what you need. This combination of Proxy and Reflect is extremely powerful — it enables reactive state systems (Vue 3 uses Proxy for its reactivity), validation layers, change tracking, lazy loading, and access control, all without modifying the original object.
The pattern is transparent: code that uses the proxy does not need to know it is not talking directly to the target. This transparency is both a strength and a risk — proxies can introduce subtle bugs if traps are incomplete or incorrect. Performance is another consideration: every intercepted operation goes through the trap function, so proxying objects in hot loops can cause measurable overhead.
Key Points
- A proxy intercepts operations on a target object via trap functions in a handler
- JavaScript has a native Proxy constructor — no manual implementation needed
- Common traps: get, set, has, deleteProperty, apply (for functions), construct
- Reflect methods provide the default behavior inside trap implementations
- Use cases: validation, lazy loading, access control, change tracking, virtual properties
- Proxies are transparent — code using the proxy does not know it is not the real object
Code Examples
Validation Proxy
function createValidatedObject(schema) { return new Proxy({}, { set(target, prop, value) { const validator = schema[prop]; if (validator && !validator(value)) { throw new TypeError(prop + ' failed validation'); } return Reflect.set(target, prop, value); }, }); } const user = createValidatedObject({ age: (v) => typeof v === 'number' && v >= 0 && v <= 150, name: (v) => typeof v === 'string' && v.length > 0, }); user.name = 'Alice'; // OK user.age = 25; // OK user.age = -5; // TypeError: age failed validation
The set trap validates every assignment. Invalid values are rejected before reaching the target.
Change Tracking Proxy
function trackChanges(target) { const changes = []; const proxy = new Proxy(target, { set(obj, prop, value) { changes.push({ prop, oldValue: obj[prop], newValue: value, timestamp: Date.now(), }); return Reflect.set(obj, prop, value); }, }); return { proxy, getChanges: () => [...changes] }; } const { proxy: config, getChanges } = trackChanges({ debug: false }); config.debug = true; config.logLevel = 'verbose'; console.log(getChanges()); // [{ prop: 'debug', oldValue: false, newValue: true, ... }, ...]
Every set operation is logged. This pattern powers reactive state systems and undo/redo features.
Lazy Loading Proxy
function createLazyLoader(loader) { let data = null; let loaded = false; return new Proxy({}, { get(target, prop) { if (!loaded) { data = loader(); loaded = true; } return Reflect.get(data, prop); }, }); } const heavyConfig = createLazyLoader(() => { console.log('Loading expensive config...'); return { apiKey: 'abc123', region: 'us-east', timeout: 5000 }; }); // Config is NOT loaded yet console.log(heavyConfig.apiKey); // "Loading expensive config..." then "abc123" console.log(heavyConfig.region); // "us-east" — no reload
The proxy defers the expensive loader call until the first property access, then caches the result.
Common Mistakes
- Forgetting to use Reflect inside traps — without it, default behavior is lost and the proxy breaks
- Creating performance bottlenecks by proxying hot-path objects accessed millions of times
- Not realizing that proxy === target is false — identity checks fail with proxies
- Using proxies when a simpler getter/setter would suffice
- Not handling all necessary traps — for example, proxying an array requires the get trap for .length
Interview Tips
- Know the native Proxy API and at least 3-4 trap names (get, set, has, apply)
- Explain how Reflect provides the default behavior that traps override
- Mention real-world uses: Vue 3 reactivity, MobX observables, API mocking
- Discuss the performance trade-off — proxies add overhead per operation