JavaScript gives you full control over object behavior using a hidden gem: the Proxy. Think of it as a trap-layer between your code and the object it interacts with β letting you intercept reads, writes, deletes, method calls, and more.
Itβs like Object.defineProperty() on steroids. Let's break it down with surgical clarity.
A Proxy wraps an object and lets you override fundamental operations.
Syntax:
const proxy = new Proxy(target, handler);
target: the object you want to wraphandler: an object with traps (interceptor methods)Hereβs a minimal example that logs every property read:
const person = { name: "Alice", age: 30 }; const proxy = new Proxy(person, { get(target, prop) { console.log(`Getting ${prop}`); return target[prop]; } }); console.log(proxy.name); // Logs: Getting name β Outputs: Alice
Enforce strict rules when setting object properties.
const user = new Proxy({}, { set(target, prop, value) { if (prop === 'age' && typeof value !== 'number') { throw new Error("Age must be a number"); } target[prop] = value; return true; } }); user.age = 25; // β user.age = "twenty"; // β Error: Age must be a number
Avoid this binding bugs in classes:
function bindMethods(obj) { return new Proxy(obj, { get(target, prop, receiver) { const value = Reflect.get(target, prop, receiver); return typeof value === 'function' ? value.bind(target) : value; } }); }
Now you can safely extract methods without losing context:
class Counter { count = 0; inc() { this.count++; } } const counter = bindMethods(new Counter()); const fn = counter.inc; fn(); // β this is bound correctly
Make an object read-only:
function readonly(obj) { return new Proxy(obj, { set() { throw new Error("Cannot modify readonly object"); }, deleteProperty() { throw new Error("Cannot delete properties"); } }); } const config = readonly({ debug: true }); config.debug = false; // β Error
Detect mutations like .push():
const list = new Proxy([], { get(target, prop) { if (prop === 'push') { return (...args) => { console.log("Pushing:", args); return Array.prototype.push.apply(target, args); }; } return Reflect.get(target, prop); } }); list.push(1); // Logs: Pushing [1]
This is foundational to reactivity engines (Vue 2 used Proxies via defineProperty; Vue 3 uses native Proxy).
Return defaults for missing keys:
const withDefault = (obj, defaultValue) => new Proxy(obj, { get(target, prop) { return prop in target ? target[prop] : defaultValue; } }); const settings = withDefault({ theme: "dark" }, "N/A"); console.log(settings.language); // Outputs: N/A
| Feature | ProxyCan Intercept |
|---|---|
| Property get/set | β |
| Method call binding | β |
| Deletion | β |
| Enumeration | β |
inoperator | β |
Object.keys() | β |
instanceof | β |
Proxy is one of the most underrated meta-programming tools in JavaScript. It's not for every use case β but when you need full behavioral control, reactive systems, or clean abstractions, it's the scalpel you want.
Test your understanding with 3 quick questions