Skip to main content

Proxy and Reflect API

Answer

Proxy allows you to intercept and customize fundamental operations on objects (property access, assignment, function calls). Reflect provides methods for these same operations, making it easy to forward default behavior.

Proxy Basics

const target = { name: "John", age: 30 };

const handler = {
get(target, property, receiver) {
console.log(`Getting ${property}`);
return target[property];
},

set(target, property, value, receiver) {
console.log(`Setting ${property} = ${value}`);
target[property] = value;
return true;
},
};

const proxy = new Proxy(target, handler);

proxy.name; // Logs: "Getting name", returns "John"
proxy.age = 31; // Logs: "Setting age = 31"

Common Traps

TrapTriggered By
getProperty read
setProperty assignment
hasin operator
deletePropertydelete operator
applyFunction call
constructnew operator
getPrototypeOfObject.getPrototypeOf()
ownKeysObject.keys(), for...in

Validation

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("Invalid age");
}
}
target[property] = value;
return true;
},
};

const person = new Proxy({}, validator);

person.age = 30; // OK
person.age = -5; // RangeError: Invalid age
person.age = "30"; // TypeError: Age must be a number

Default Values

const withDefaults = (target, defaults) => {
return new Proxy(target, {
get(target, property) {
return property in target ? target[property] : defaults[property];
},
});
};

const settings = withDefaults(
{},
{
theme: "dark",
fontSize: 14,
language: "en",
}
);

console.log(settings.theme); // "dark"
console.log(settings.fontSize); // 14
settings.theme = "light";
console.log(settings.theme); // "light"

Logging / Debugging

function createLogger(target) {
return new Proxy(target, {
get(target, property, receiver) {
const value = Reflect.get(target, property, receiver);
console.log(`[GET] ${property} = ${JSON.stringify(value)}`);
return value;
},

set(target, property, value, receiver) {
console.log(`[SET] ${property} = ${JSON.stringify(value)}`);
return Reflect.set(target, property, value, receiver);
},

deleteProperty(target, property) {
console.log(`[DELETE] ${property}`);
return Reflect.deleteProperty(target, property);
},
});
}

const user = createLogger({ name: "John" });
user.name; // [GET] name = "John"
user.age = 30; // [SET] age = 30
delete user.age; // [DELETE] age

Reflect API

// Reflect provides the same operations as Proxy traps
// Makes it easy to forward to default behavior

const obj = { name: "John" };

// Old way
obj.name; // "John"
obj.name = "Jane"; // assignment
"name" in obj; // true
delete obj.age; // true

// Reflect way
Reflect.get(obj, "name"); // "John"
Reflect.set(obj, "name", "Jane"); // true
Reflect.has(obj, "name"); // true
Reflect.deleteProperty(obj, "age"); // true

// Why use Reflect?
// 1. Returns boolean instead of throwing
// 2. Consistent API for metaprogramming
// 3. Works properly with Proxy

Observable Objects

function observable(target, callback) {
return new Proxy(target, {
set(target, property, value, receiver) {
const oldValue = target[property];
const result = Reflect.set(target, property, value, receiver);

if (oldValue !== value) {
callback(property, oldValue, value);
}

return result;
},
});
}

const data = observable({ count: 0 }, (prop, oldVal, newVal) => {
console.log(`${prop} changed from ${oldVal} to ${newVal}`);
updateUI();
});

data.count = 1; // "count changed from 0 to 1"
data.count = 2; // "count changed from 1 to 2"

Private Properties

function createPrivate(target, privateKeys = []) {
return new Proxy(target, {
get(target, property) {
if (privateKeys.includes(property)) {
throw new Error(`Cannot access private property: ${property}`);
}
return Reflect.get(target, property);
},

set(target, property, value) {
if (privateKeys.includes(property)) {
throw new Error(`Cannot set private property: ${property}`);
}
return Reflect.set(target, property, value);
},

ownKeys(target) {
return Reflect.ownKeys(target).filter(
(key) => !privateKeys.includes(key)
);
},
});
}

const user = createPrivate({ name: "John", _password: "secret" }, [
"_password",
]);

console.log(user.name); // "John"
console.log(user._password); // Error: Cannot access private property
console.log(Object.keys(user)); // ["name"]

Function Proxy

const loggedFunction = new Proxy(originalFunction, {
apply(target, thisArg, args) {
console.log(`Calling ${target.name} with:`, args);
const result = Reflect.apply(target, thisArg, args);
console.log(`Result:`, result);
return result;
},
});

// Memoization via Proxy
function memoize(fn) {
const cache = new Map();

return new Proxy(fn, {
apply(target, thisArg, args) {
const key = JSON.stringify(args);

if (cache.has(key)) {
console.log("Cache hit");
return cache.get(key);
}

const result = Reflect.apply(target, thisArg, args);
cache.set(key, result);
return result;
},
});
}

const expensiveCalc = memoize((n) => {
console.log("Computing...");
return n * n;
});

expensiveCalc(5); // Computing... 25
expensiveCalc(5); // Cache hit 25

Revocable Proxy

// Can be revoked after creation
const { proxy, revoke } = Proxy.revocable(target, handler);

proxy.name; // Works
revoke(); // Revoke access
proxy.name; // TypeError: Cannot perform 'get' on a proxy that has been revoked

// Use case: Temporary access
function grantTemporaryAccess(data, duration) {
const { proxy, revoke } = Proxy.revocable(data, {});

setTimeout(revoke, duration);

return proxy;
}

const tempAccess = grantTemporaryAccess(secretData, 5000);
// Access revoked after 5 seconds

Key Points

  • Proxy intercepts fundamental object operations
  • Reflect provides matching methods for default behavior
  • Use Proxies for validation, logging, caching
  • Revocable proxies for temporary access
  • Performance: Proxies add overhead
  • Always use Reflect in handlers for correctness