JavaScript 58 🧬 Proxy and Reflect
const target = { name: 'Alice', age: 30 };
const proxy = new Proxy(target, {
get(obj, prop) {
console.log(`Getting ${prop}`);
return obj[prop];
},
set(obj, prop, value) {
console.log(`Setting ${prop} to ${value}`);
obj[prop] = value;
return true;
}
});
console.log(proxy.name);
proxy.age = 31;
console.log(target.age);
const handler = {
has(obj, prop) {
return prop in obj;
},
deleteProperty(obj, prop) {
delete obj[prop];
return true;
}
};
const proxy2 = new Proxy(target, handler);
console.log('name' in proxy2);
delete proxy2.age;
console.log('age' in proxy2);
const validator = new Proxy({}, {
set(obj, prop, value) {
if (prop === 'age' && typeof value !== 'number') {
throw new TypeError('Age must be a number');
}
obj[prop] = value;
return true;
}
});
validator.age = 30;
console.log(validator.age);
const defaults = new Proxy({}, {
get(obj, prop) {
return obj[prop] ?? `default-${prop}`;
}
});
console.log(defaults.name);
console.log(defaults.foo);
const reflectTest = {
name: 'Alice'
};
console.log(Reflect.get(reflectTest, 'name'));
console.log(Reflect.has(reflectTest, 'name'));
Reflect.set(reflectTest, 'age', 30);
console.log(reflectTest.age);
console.log(Reflect.ownKeys(reflectTest));
A Proxy wraps an object and intercepts its operations — reads, writes, deletes, function calls, and more. A Reflect object provides the default behaviors that a Proxy can use — the same operations, but with a clean API. Together they’re the foundation of JavaScript metaprogramming.
Key point: A Proxy is a trap-based interceptor. It doesn’t change the target object — it wraps it. Every operation goes through a handler function you define. Reflect is the toolbox of default behaviors you’d otherwise implement by hand.
a – What is a Proxy
A Proxy creates a transparent wrapper around an object. It intercepts fundamental operations and lets you customize what they do.
Basic syntax:
const proxy = new Proxy(target, handler);
target— the object being wrappedhandler— an object with traps (interceptor functions)
The simplest proxy:
const target = { name: 'Alice' };
const proxy = new Proxy(target, {});
console.log(proxy.name);
// [ 'Alice' ]
With an empty handler, the proxy behaves exactly like the target.
Why Proxies matter:
- Intercept any operation on an object
- Validate writes before they happen
- Provide defaults for missing properties
- Log or debug access patterns
- Implement lazy loading, caching, immutability
- Power frameworks like Vue 3 and MobX
Proxy vs Object.defineProperty:
Object.defineProperty intercepts a single property. Proxy intercepts everything — all properties, all operations.
| Feature | defineProperty | Proxy |
|---|---|---|
| Per-property | ✅ | ✅ (via traps) |
| Whole object | ❌ | ✅ |
| Method calls | ❌ | ✅ |
in operator | ❌ | ✅ |
delete | ❌ | ✅ |
ownKeys | ❌ | ✅ |
The available traps:
| Trap | Intercepts |
|---|---|
get | Property read |
set | Property write |
has | in operator |
deleteProperty | delete |
ownKeys | Object.keys, getOwnPropertyNames |
getOwnPropertyDescriptor | Descriptor lookup |
defineProperty | Object.defineProperty |
getPrototypeOf | Prototype read |
setPrototypeOf | Prototype write |
isExtensible | Extensibility check |
preventExtensions | Make non-extensible |
apply | Function call |
construct | new operator |
The get trap:
const proxy = new Proxy({ name: 'Alice' }, {
get(target, prop) {
console.log(`Reading ${prop}`);
return target[prop];
}
});
console.log(proxy.name);
// [ Reading name ]
// [ 'Alice' ]
The set trap:
const proxy = new Proxy({}, {
set(target, prop, value) {
console.log(`Writing ${prop} = ${value}`);
target[prop] = value;
return true;
}
});
proxy.age = 30;
// [ Writing age = 30 ]
The trap must return true for the write to succeed. Returning false throws a TypeError in strict mode.
The has trap — the in operator:
const proxy = new Proxy({ name: 'Alice' }, {
has(target, prop) {
return prop in target;
}
});
console.log('name' in proxy);
// [ true ]
console.log('age' in proxy);
// [ false ]
The deleteProperty trap:
const proxy = new Proxy({ name: 'Alice' }, {
deleteProperty(target, prop) {
console.log(`Deleting ${prop}`);
delete target[prop];
return true;
}
});
delete proxy.name;
// [ Deleting name ]
The ownKeys trap:
const proxy = new Proxy({ name: 'Alice', age: 30 }, {
ownKeys(target) {
return Object.keys(target);
}
});
console.log(Object.keys(proxy));
// [ [ 'name', 'age' ] ]
Why the target and proxy stay in sync:
Operations on the proxy forward to the target by default. If a trap doesn’t do anything, the target is unchanged.
const target = { a: 1 };
const proxy = new Proxy(target, {});
proxy.b = 2;
console.log(target.b);
// [ 2 ]
When to use Proxies:
| Use case | Example |
|---|---|
| Validation | Check writes before they happen |
| Defaults | Fill in missing values |
| Logging | Trace reads and writes |
| Caching | Memoize property access |
| Access control | Block certain keys |
| Immutability | Block all writes |
| Lazy loading | Fetch on access |
| Observables | React to changes |
Basic example — read/write logging:
function createLogger(target) {
return new Proxy(target, {
get(obj, prop) {
console.log(`get ${String(prop)}`);
return Reflect.get(obj, prop);
},
set(obj, prop, value) {
console.log(`set ${String(prop)} = ${value}`);
return Reflect.set(obj, prop, value);
}
});
}
const logged = createLogger({ name: 'Alice' });
logged.name;
// [ get name ]
logged.age = 30;
// [ set age = 30 ]
b – The Proxy traps
Each trap intercepts a specific operation. Here are the important ones.
get(target, prop, receiver) — reading:
const proxy = new Proxy({ name: 'Alice' }, {
get(target, prop, receiver) {
if (typeof prop === 'string' && !(prop in target)) {
return `missing: ${prop}`;
}
return Reflect.get(target, prop, receiver);
}
});
console.log(proxy.name);
// [ 'Alice' ]
console.log(proxy.missing);
// [ 'missing: missing' ]
The receiver is usually the proxy itself — useful for inherited getters.
set(target, prop, value, receiver) — writing:
const proxy = new Proxy({}, {
set(target, prop, value, receiver) {
if (prop === 'age' && typeof value !== 'number') {
throw new TypeError('age must be a number');
}
return Reflect.set(target, prop, value, receiver);
}
});
proxy.age = 30; // ✅
proxy.age = 'thirty'; // ❌ TypeError
The trap returns true to allow, false to reject (strict mode throws).
has(target, prop) — in operator:
const hidden = new Set(['secret']);
const proxy = new Proxy({ public: 1, secret: 2 }, {
has(target, prop) {
if (hidden.has(prop)) return false;
return Reflect.has(target, prop);
}
});
console.log('public' in proxy);
// [ true ]
console.log('secret' in proxy);
// [ false ] ← hidden
deleteProperty(target, prop) — delete:
const locked = new Set(['id']);
const proxy = new Proxy({ id: 1, name: 'Alice' }, {
deleteProperty(target, prop) {
if (locked.has(prop)) {
throw new Error(`Cannot delete ${prop}`);
}
return Reflect.deleteProperty(target, prop);
}
});
delete proxy.name; // ✅
delete proxy.id; // ❌ Error
ownKeys(target) — key enumeration:
const proxy = new Proxy({ a: 1, b: 2, _c: 3 }, {
ownKeys(target) {
return Reflect.ownKeys(target).filter(k => !String(k).startsWith('_'));
}
});
console.log(Object.keys(proxy));
// [ [ 'a', 'b' ] ]
getOwnPropertyDescriptor — descriptor lookup:
const proxy = new Proxy({ a: 1 }, {
getOwnPropertyDescriptor(target, prop) {
const desc = Reflect.getOwnPropertyDescriptor(target, prop);
if (desc) desc.writable = false;
return desc;
}
});
defineProperty — descriptor setting:
const proxy = new Proxy({}, {
defineProperty(target, prop, descriptor) {
console.log(`Defining ${String(prop)}`);
return Reflect.defineProperty(target, prop, descriptor);
}
});
Object.defineProperty(proxy, 'x', { value: 42 });
// [ Defining x ]
apply(target, thisArg, args) — function call:
For a proxy around a function:
const fn = (a, b) => a + b;
const proxy = new Proxy(fn, {
apply(target, thisArg, args) {
console.log(`Called with ${args}`);
return Reflect.apply(target, thisArg, args);
}
});
console.log(proxy(2, 3));
// [ Called with 2,3 ]
// [ 5 ]
construct(target, args, newTarget) — new:
class Person {
constructor(name) { this.name = name; }
}
const proxy = new Proxy(Person, {
construct(target, args, newTarget) {
console.log(`Constructing with ${args}`);
return Reflect.construct(target, args, newTarget);
}
});
const p = new proxy('Alice');
// [ Constructing with Alice ]
console.log(p.name);
// [ 'Alice' ]
getPrototypeOf and setPrototypeOf:
const proto = { greet() { return 'hi'; } };
const proxy = new Proxy({}, {
getPrototypeOf(target) {
return proto;
}
});
console.log(proxy.greet());
// [ 'hi' ]
isExtensible and preventExtensions:
let locked = false;
const proxy = new Proxy({}, {
isExtensible(target) {
return Reflect.isExtensible(target) && !locked;
},
preventExtensions(target) {
locked = true;
return Reflect.preventExtensions(target);
}
});
get for defaults — a common pattern:
const withDefaults = new Proxy({}, {
get(target, prop) {
if (prop in target) return target[prop];
return `default-${String(prop)}`;
}
});
console.log(withDefaults.name);
// [ 'default-name' ]
console.log(withDefaults.foo);
// [ 'default-foo' ]
set for validation — a common pattern:
const strict = new Proxy({}, {
set(target, prop, value) {
if (typeof prop === 'string' && prop.startsWith('_')) {
throw new Error('Private props cannot be set directly');
}
return Reflect.set(target, prop, value);
}
});
get for observability — track reads:
function observe(obj, onChange) {
return new Proxy(obj, {
get(target, prop, receiver) {
onChange('get', prop);
return Reflect.get(target, prop, receiver);
},
set(target, prop, value, receiver) {
onChange('set', prop, value);
return Reflect.set(target, prop, value, receiver);
}
});
}
const state = observe({ count: 0 }, (op, prop, value) => {
console.log(`${op}: ${String(prop)}${value ? ' = ' + value : ''}`);
});
state.count;
// [ get: count ]
state.count = 1;
// [ set: count = 1 ]
ownKeys for private keys — hide from enumeration:
const proxy = new Proxy({ a: 1, b: 2, secret: 3 }, {
ownKeys(target) {
return Reflect.ownKeys(target).filter(k => k !== 'secret');
},
getOwnPropertyDescriptor(target, prop) {
if (prop === 'secret') return undefined;
return Reflect.getOwnPropertyDescriptor(target, prop);
}
});
console.log(Object.keys(proxy));
// [ [ 'a', 'b' ] ]
All traps and their defaults:
| Trap | Default behavior |
|---|---|
get | Reflect.get(target, prop, receiver) |
set | Reflect.set(target, prop, value, receiver) |
has | Reflect.has(target, prop) |
deleteProperty | Reflect.deleteProperty(target, prop) |
ownKeys | Reflect.ownKeys(target) |
getOwnPropertyDescriptor | Reflect.getOwnPropertyDescriptor(target, prop) |
defineProperty | Reflect.defineProperty(target, prop, desc) |
getPrototypeOf | Reflect.getPrototypeOf(target) |
setPrototypeOf | Reflect.setPrototypeOf(target, proto) |
isExtensible | Reflect.isExtensible(target) |
preventExtensions | Reflect.preventExtensions(target) |
apply | Reflect.apply(target, thisArg, args) |
construct | Reflect.construct(target, args, newTarget) |
The Reflect API:
Reflect provides functions that mirror every proxy trap. They give the default behavior that proxies often forward to.
Reflect methods:
| Method | Purpose |
|---|---|
Reflect.get(obj, prop) | Read property |
Reflect.set(obj, prop, value) | Write property |
Reflect.has(obj, prop) | in operator |
Reflect.deleteProperty(obj, prop) | delete |
Reflect.ownKeys(obj) | All keys |
Reflect.getOwnPropertyDescriptor(obj, prop) | Get descriptor |
Reflect.defineProperty(obj, prop, desc) | Define |
Reflect.getPrototypeOf(obj) | Get prototype |
Reflect.setPrototypeOf(obj, proto) | Set prototype |
Reflect.isExtensible(obj) | Can extend? |
Reflect.preventExtensions(obj) | Lock |
Reflect.apply(fn, thisArg, args) | Call function |
Reflect.construct(Ctor, args) | new |
Why Reflect exists:
Before Reflect, some operations didn’t have a clean function form:
// Old way to call a function
fn.apply(thisArg, args);
// Old way to get a prototype
Object.getPrototypeOf(obj);
// Old way to check "in"
'key' in obj;
// Old way to delete
delete obj.key;
Reflect provides consistent functions that match the proxy traps:
Reflect.apply(fn, thisArg, args);
Reflect.getPrototypeOf(obj);
Reflect.has(obj, 'key');
Reflect.deleteProperty(obj, 'key');
Reflect preserves the receiver:
The receiver argument in Reflect.get matters when the target has getters. It ensures this inside the getter points to the proxy, not the target:
const base = {
get name() { return this._name; },
_name: 'Alice'
};
const proxy = new Proxy(base, {
get(target, prop, receiver) {
return Reflect.get(target, prop, receiver); // ✅ passes receiver
}
});
console.log(proxy.name);
// [ 'Alice' ]
Without the receiver, this._name would look for _name on target, not proxy — but in this case both are fine.
When to use Reflect:
- Inside a proxy trap — to forward to the default behavior
- When you need a function form of an operation
- When you want consistent return values
When NOT to use Reflect:
- Simple operations have cleaner syntax (
in,delete, etc.) - For most code,
obj.propandobj.prop = valueare clearer
Example — validating writes with Reflect:
const schema = {
age: v => typeof v === 'number' && v >= 0,
name: v => typeof v === 'string' && v.length > 0
};
const proxy = new Proxy({}, {
set(target, prop, value, receiver) {
const validator = schema[prop];
if (validator && !validator(value)) {
throw new TypeError(`Invalid value for ${prop}: ${value}`);
}
return Reflect.set(target, prop, value, receiver);
}
});
proxy.name = 'Alice'; // ✅
proxy.age = 30; // ✅
try {
proxy.age = -1;
} catch (err) {
console.log(err.message);
// [ Invalid value for age: -1 ]
}
Example — read-only proxy:
function readOnly(target) {
return new Proxy(target, {
set() {
throw new Error('Cannot modify read-only object');
},
deleteProperty() {
throw new Error('Cannot delete from read-only object');
},
defineProperty() {
throw new Error('Cannot define on read-only object');
}
});
}
const ro = readOnly({ name: 'Alice' });
console.log(ro.name);
// [ 'Alice' ]
try {
ro.name = 'Bob';
} catch (err) {
console.log(err.message);
// [ Cannot modify read-only object ]
}
Example — negative array indices:
function withNegativeIndices(arr) {
return new Proxy(arr, {
get(target, prop) {
if (typeof prop === 'string' && /^-\d+$/.test(prop)) {
const idx = target.length + Number(prop);
return target[idx];
}
return Reflect.get(target, prop);
},
set(target, prop, value) {
if (typeof prop === 'string' && /^-\d+$/.test(prop)) {
const idx = target.length + Number(prop);
target[idx] = value;
return true;
}
return Reflect.set(target, prop, value);
}
});
}
const arr = withNegativeIndices([1, 2, 3]);
console.log(arr[-1]);
// [ 3 ]
arr[-1] = 99;
console.log(arr[2]);
// [ 99 ]
Example — observable state (Vue-style):
function reactive(obj, onChange) {
return new Proxy(obj, {
get(target, prop, receiver) {
const value = Reflect.get(target, prop, receiver);
if (typeof value === 'object' && value !== null) {
return reactive(value, onChange);
}
return value;
},
set(target, prop, value, receiver) {
const oldValue = target[prop];
const result = Reflect.set(target, prop, value, receiver);
if (oldValue !== value) {
onChange(prop, value, oldValue);
}
return result;
}
});
}
const state = reactive({ count: 0 }, (prop, val, old) => {
console.log(`${String(prop)}: ${old} → ${val}`);
});
state.count = 1;
// [ count: 0 → 1 ]
state.count = 2;
// [ count: 1 → 2 ]
c – Common Proxy and Reflect patterns
These patterns come up constantly in real applications and libraries.
Pattern 1 — Default values:
function withDefaults(target, defaults) {
return new Proxy(target, {
get(obj, prop) {
return prop in obj ? obj[prop] : defaults[prop];
}
});
}
const config = withDefaults({ port: 3000 }, { host: 'localhost' });
console.log(config.port); // 3000
console.log(config.host); // localhost
Pattern 2 — Validation:
function validate(target, rules) {
return new Proxy(target, {
set(obj, prop, value) {
const rule = rules[prop];
if (rule && !rule(value)) {
throw new TypeError(`Invalid ${String(prop)}`);
}
return Reflect.set(obj, prop, value);
}
});
}
const user = validate({}, {
age: v => typeof v === 'number' && v >= 0,
email: v => /@/.test(v)
});
Pattern 3 — Logging:
function log(target, label = 'obj') {
return new Proxy(target, {
get(obj, prop) {
console.log(`${label}.${String(prop)}`);
return Reflect.get(obj, prop);
},
set(obj, prop, value) {
console.log(`${label}.${String(prop)} = ${value}`);
return Reflect.set(obj, prop, value);
}
});
}
Pattern 4 — Read-only wrapper:
function readOnly(target) {
return new Proxy(target, {
set() { throw new Error('read-only'); },
deleteProperty() { throw new Error('read-only'); },
defineProperty() { throw new Error('read-only'); }
});
}
Pattern 5 — Hidden private fields:
function hide(target, hidden) {
return new Proxy(target, {
has(obj, prop) {
return !hidden.has(prop) && Reflect.has(obj, prop);
},
ownKeys(obj) {
return Reflect.ownKeys(obj).filter(k => !hidden.has(k));
},
getOwnPropertyDescriptor(obj, prop) {
if (hidden.has(prop)) return undefined;
return Reflect.getOwnPropertyDescriptor(obj, prop);
}
});
}
Pattern 6 — Lazy loading:
function lazy(getLoader) {
let loaded = null;
return new Proxy({}, {
get(obj, prop) {
loaded ??= getLoader();
return Reflect.get(loaded, prop);
}
});
}
Pattern 7 — Function mocking:
function trackCalls(fn) {
const calls = [];
const proxy = new Proxy(fn, {
apply(target, thisArg, args) {
calls.push(args);
return Reflect.apply(target, thisArg, args);
}
});
proxy.calls = calls;
return proxy;
}
const add = trackCalls((a, b) => a + b);
add(1, 2);
add(3, 4);
console.log(add.calls);
// [ [ [ 1, 2 ], [ 3, 4 ] ] ]
Pattern 8 — Array with negative indices:
function negIndex(arr) {
return new Proxy(arr, {
get(target, prop) {
if (typeof prop === 'string' && /^-\d+$/.test(prop)) {
return target[target.length + Number(prop)];
}
return Reflect.get(target, prop);
}
});
}
Pattern 9 — Access control:
function withPermissions(target, perms) {
return new Proxy(target, {
get(obj, prop) {
if (!perms.read?.includes(prop)) {
throw new Error(`No read access to ${String(prop)}`);
}
return Reflect.get(obj, prop);
},
set(obj, prop, value) {
if (!perms.write?.includes(prop)) {
throw new Error(`No write access to ${String(prop)}`);
}
return Reflect.set(obj, prop, value);
}
});
}
Pattern 10 — Memoization on access:
function cached(compute) {
const cache = new Map();
return new Proxy({}, {
get(obj, prop) {
if (!cache.has(prop)) {
cache.set(prop, compute(prop));
}
return cache.get(prop);
}
});
}
Pattern 11 — Type coercion with get:
const stringify = new Proxy({ x: 10, y: 20 }, {
get(target, prop) {
if (prop === Symbol.toPrimitive) {
return () => JSON.stringify(target);
}
return Reflect.get(target, prop);
}
});
console.log(`${stringify}`);
// [ '{"x":10,"y":20}' ]
Pattern 12 — Combining Reflect with inheritance:
const parent = { greet() { return 'hi'; } };
const child = new Proxy(Object.create(parent), {
get(target, prop, receiver) {
console.log(`Accessing ${String(prop)}`);
return Reflect.get(target, prop, receiver);
}
});
console.log(child.greet());
// [ Accessing greet ]
// [ 'hi' ]
Reflect.get with receiver ensures inherited getters see the proxy.
Pattern 13 — Defaults with has:
function defaultValue(target, fallback) {
return new Proxy(target, {
get(obj, prop) {
if (prop in obj) return obj[prop];
return fallback;
}
});
}
Pattern 14 — Revocable proxy:
const { proxy, revoke } = Proxy.revocable({ name: 'Alice' }, {});
console.log(proxy.name);
// [ 'Alice' ]
revoke();
console.log(proxy.name);
// TypeError: Cannot perform 'get' on a proxy that has been revoked
Pattern 15 — Immutable state:
function immutable(target) {
return new Proxy(target, {
set() { throw new Error('immutable'); },
deleteProperty() { throw new Error('immutable'); },
defineProperty() { throw new Error('immutable'); },
setPrototypeOf() { throw new Error('immutable'); }
});
}
Pattern 16 — Auto-populating objects:
const autoFill = new Proxy({}, {
get(target, prop) {
if (!(prop in target)) {
target[prop] = { name: String(prop) };
}
return target[prop];
}
});
console.log(autoFill.users);
// [ { name: 'users' } ]
console.log(autoFill.posts);
// [ { name: 'posts' } ]
Pattern 17 — Path tracking for state libraries:
function trackPath(obj, path = '') {
return new Proxy(obj, {
get(target, prop, receiver) {
const value = Reflect.get(target, prop, receiver);
const newPath = path ? `${path}.${String(prop)}` : String(prop);
if (value && typeof value === 'object') {
return trackPath(value, newPath);
}
return value;
}
});
}
Pattern 18 — Lazy nested objects:
const lazyObj = new Proxy({}, {
get(target, prop) {
target[prop] ??= new Proxy({}, {
get(t, p) {
return `${String(prop)}.${String(p)}`;
}
});
return target[prop];
}
});
console.log(lazyObj.user.name);
// [ 'user.name' ]
Pattern 19 — Function argument validation:
function validateArgs(fn, validators) {
return new Proxy(fn, {
apply(target, thisArg, args) {
validators.forEach((check, i) => {
if (!check(args[i])) {
throw new TypeError(`Argument ${i} invalid`);
}
});
return Reflect.apply(target, thisArg, args);
}
});
}
Pattern 20 — Full observable example:
function observable(target, onChange) {
const handler = {
get(obj, prop, receiver) {
const value = Reflect.get(obj, prop, receiver);
if (value && typeof value === 'object') {
return observable(value, onChange);
}
return value;
},
set(obj, prop, value, receiver) {
const oldValue = obj[prop];
const result = Reflect.set(obj, prop, value, receiver);
if (oldValue !== value) {
onChange(prop, value, oldValue);
}
return result;
}
};
return new Proxy(target, handler);
}
const state = observable(
{ user: { name: 'Alice' } },
(prop, val, old) => console.log(`${String(prop)}: ${old} → ${val}`)
);
state.user.name = 'Bob';
// [ name: Alice → Bob ]
When to use Proxy and Reflect:
| Situation | Use |
|---|---|
| Validation on write | Proxy set |
| Defaults for reads | Proxy get |
| Logging access | Proxy get / set |
| Read-only objects | Proxy set throws |
| Hiding keys | Proxy ownKeys |
Custom in behavior | Proxy has |
| Function mocking | Proxy apply |
| Framework reactivity | Proxy get / set |
| Forwarding to defaults | Reflect |
| Accessing descriptors | Reflect |
| Calling functions reflectively | Reflect |
Common pitfalls:
- Proxy traps must match the target’s invariants — e.g., can’t report non-configurable props as missing
ownKeysmust include non-configurable keys — otherwise TypeError- Reflect helps keep invariants — use it inside traps
- Proxies are not transparent —
===compares the proxy, not the target - Proxy performance is lower than plain objects — use sparingly
- Revocable proxies can be disabled —
Proxy.revocable
Complete Example Session
// ============================================
// PART 1: BASIC GET TRAP
// ============================================
const target = { name: 'Alice' };
const proxy = new Proxy(target, {
get(obj, prop) {
console.log(`Getting ${prop}`);
return obj[prop];
}
});
console.log(proxy.name);
// [ Getting name ]
// [ 'Alice' ]
// ============================================
// PART 2: SET TRAP
// ============================================
const proxy2 = new Proxy({}, {
set(obj, prop, value) {
console.log(`Setting ${prop} = ${value}`);
obj[prop] = value;
return true;
}
});
proxy2.age = 30;
// [ Setting age = 30 ]
// ============================================
// PART 3: HAS TRAP
// ============================================
const proxy3 = new Proxy({ name: 'Alice' }, {
has(obj, prop) {
return prop in obj;
}
});
console.log('name' in proxy3);
// [ true ]
console.log('age' in proxy3);
// [ false ]
// ============================================
// PART 4: DELETE TRAP
// ============================================
const proxy4 = new Proxy({ name: 'Alice' }, {
deleteProperty(obj, prop) {
console.log(`Deleting ${prop}`);
return Reflect.deleteProperty(obj, prop);
}
});
delete proxy4.name;
// [ Deleting name ]
// ============================================
// PART 5: VALIDATION
// ============================================
const strict = new Proxy({}, {
set(obj, prop, value) {
if (prop === 'age' && typeof value !== 'number') {
throw new TypeError('age must be a number');
}
return Reflect.set(obj, prop, value);
}
});
strict.age = 30;
console.log(strict.age);
// [ 30 ]
try {
strict.age = 'thirty';
} catch (err) {
console.log(err.message);
}
// [ age must be a number ]
// ============================================
// PART 6: DEFAULTS
// ============================================
const defaults = new Proxy({}, {
get(obj, prop) {
return obj[prop] ?? `default-${String(prop)}`;
}
});
console.log(defaults.name);
// [ 'default-name' ]
console.log(defaults.foo);
// [ 'default-foo' ]
// ============================================
// PART 7: REFLECT
// ============================================
const obj = { name: 'Alice' };
console.log(Reflect.get(obj, 'name'));
// [ 'Alice' ]
console.log(Reflect.has(obj, 'name'));
// [ true ]
Reflect.set(obj, 'age', 30);
console.log(obj.age);
// [ 30 ]
console.log(Reflect.ownKeys(obj));
// [ [ 'name', 'age' ] ]
// ============================================
// PART 8: APPLY TRAP
// ============================================
const fn = (a, b) => a + b;
const fnProxy = new Proxy(fn, {
apply(target, thisArg, args) {
console.log(`Called with ${args}`);
return Reflect.apply(target, thisArg, args);
}
});
console.log(fnProxy(2, 3));
// [ Called with 2,3 ]
// [ 5 ]
// ============================================
// PART 9: CONSTRUCT TRAP
// ============================================
class Person {
constructor(name) { this.name = name; }
}
const CtorProxy = new Proxy(Person, {
construct(target, args) {
console.log(`Constructing ${args}`);
return Reflect.construct(target, args);
}
});
const p = new CtorProxy('Alice');
// [ Constructing Alice ]
console.log(p.name);
// [ 'Alice' ]
// ============================================
// PART 10: READ-ONLY
// ============================================
function readOnly(target) {
return new Proxy(target, {
set() { throw new Error('read-only'); },
deleteProperty() { throw new Error('read-only'); }
});
}
const ro = readOnly({ name: 'Alice' });
console.log(ro.name);
// [ 'Alice' ]
try {
ro.name = 'Bob';
} catch (err) {
console.log(err.message);
}
// [ read-only ]
// ============================================
// PART 11: LOGGING PROXY
// ============================================
function log(target) {
return new Proxy(target, {
get(obj, prop) {
console.log(`get ${String(prop)}`);
return Reflect.get(obj, prop);
},
set(obj, prop, value) {
console.log(`set ${String(prop)} = ${value}`);
return Reflect.set(obj, prop, value);
}
});
}
const logged = log({ name: 'Alice' });
logged.name;
// [ get name ]
logged.age = 30;
// [ set age = 30 ]
// ============================================
// PART 12: OBSERVABLE
// ============================================
function observable(target, onChange) {
return new Proxy(target, {
get(obj, prop, receiver) {
return Reflect.get(obj, prop, receiver);
},
set(obj, prop, value, receiver) {
const old = obj[prop];
const result = Reflect.set(obj, prop, value, receiver);
if (old !== value) onChange(prop, value, old);
return result;
}
});
}
const state = observable({ count: 0 }, (prop, val, old) => {
console.log(`${String(prop)}: ${old} → ${val}`);
});
state.count = 1;
// [ count: 0 → 1 ]
// ============================================
// PART 13: REVOCABLE
// ============================================
const { proxy: revocableProxy, revoke } = Proxy.revocable({ a: 1 }, {});
console.log(revocableProxy.a);
// [ 1 ]
revoke();
try {
console.log(revocableProxy.a);
} catch (err) {
console.log(err.message);
}
// [ Cannot perform 'get' on a proxy that has been revoked ]
// ============================================
// PART 14: NEGATIVE INDEX ARRAY
// ============================================
function negIndex(arr) {
return new Proxy(arr, {
get(target, prop) {
if (typeof prop === 'string' && /^-\d+$/.test(prop)) {
return target[target.length + Number(prop)];
}
return Reflect.get(target, prop);
}
});
}
const arr = negIndex([1, 2, 3]);
console.log(arr[-1]);
// [ 3 ]
// ============================================
// PART 15: HIDE KEYS
// ============================================
const hidden = new Set(['secret']);
const hideProxy = new Proxy({ a: 1, secret: 2 }, {
has(obj, prop) {
return !hidden.has(prop) && Reflect.has(obj, prop);
},
ownKeys(obj) {
return Reflect.ownKeys(obj).filter(k => !hidden.has(k));
},
getOwnPropertyDescriptor(obj, prop) {
if (hidden.has(prop)) return undefined;
return Reflect.getOwnPropertyDescriptor(obj, prop);
}
});
console.log(Object.keys(hideProxy));
// [ [ 'a' ] ]
console.log('secret' in hideProxy);
// [ false ]
// ============================================
// PART 16: FUNCTION TRACKING
// ============================================
function trackCalls(fn) {
const calls = [];
const proxy = new Proxy(fn, {
apply(target, thisArg, args) {
calls.push(args);
return Reflect.apply(target, thisArg, args);
}
});
proxy.calls = calls;
return proxy;
}
const add = trackCalls((a, b) => a + b);
add(1, 2);
add(3, 4);
console.log(add.calls.length);
// [ 2 ]
// ============================================
// PART 17: AUTO-VIVIFICATION
// ============================================
const auto = new Proxy({}, {
get(target, prop) {
if (!(prop in target)) {
target[prop] = {};
}
return target[prop];
}
});
auto.users.name = 'Alice';
console.log(auto.users.name);
// [ 'Alice' ]
// ============================================
// PART 18: REFLECT.APPLY
// ============================================
function greet(greeting) {
return `${greeting}, ${this.name}`;
}
const result = Reflect.apply(greet, { name: 'Alice' }, ['Hello']);
console.log(result);
// [ 'Hello, Alice' ]
// ============================================
// PART 19: REFLECT.CONSTRUCT
// ============================================
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
}
const pt = Reflect.construct(Point, [1, 2]);
console.log(pt.x, pt.y);
// [ 1 2 ]
// ============================================
// PART 20: FULL SCRIPT
// ============================================
const target58 = { name: 'Alice', age: 30 };
const proxy58 = new Proxy(target58, {
get(obj, prop) {
console.log(`Getting ${String(prop)}`);
return obj[prop];
},
set(obj, prop, value) {
console.log(`Setting ${String(prop)} to ${value}`);
obj[prop] = value;
return true;
}
});
console.log(proxy58.name);
proxy58.age = 31;
console.log(target58.age);
const proxy58b = new Proxy(target58, {
has(obj, prop) { return prop in obj; },
deleteProperty(obj, prop) { return Reflect.deleteProperty(obj, prop); }
});
console.log('name' in proxy58b);
delete proxy58b.age;
console.log('age' in proxy58b);
const validator58 = new Proxy({}, {
set(obj, prop, value) {
if (prop === 'age' && typeof value !== 'number') {
throw new TypeError('Age must be a number');
}
obj[prop] = value;
return true;
}
});
validator58.age = 30;
console.log(validator58.age);
const defaults58 = new Proxy({}, {
get(obj, prop) {
return obj[prop] ?? `default-${String(prop)}`;
}
});
console.log(defaults58.name);
console.log(defaults58.foo);
const reflectTest58 = { name: 'Alice' };
console.log(Reflect.get(reflectTest58, 'name'));
console.log(Reflect.has(reflectTest58, 'name'));
Reflect.set(reflectTest58, 'age', 30);
console.log(reflectTest58.age);
console.log(Reflect.ownKeys(reflectTest58));
Quick Reference
Proxy Basics
| Syntax | Meaning |
|---|---|
new Proxy(target, handler) | Create proxy |
Proxy.revocable(target, handler) | Revocable proxy |
proxy.operation | Intercepted |
target.operation | Original, untouched |
All Traps
| Trap | Intercepts |
|---|---|
get | Read |
set | Write |
has | in |
deleteProperty | delete |
ownKeys | Key enumeration |
getOwnPropertyDescriptor | Descriptor read |
defineProperty | Descriptor write |
getPrototypeOf | Prototype read |
setPrototypeOf | Prototype write |
isExtensible | Extensibility check |
preventExtensions | Lock object |
apply | Function call |
construct | new |
Reflect Methods
| Method | Purpose |
|---|---|
Reflect.get(o, p) | Read |
Reflect.set(o, p, v) | Write |
Reflect.has(o, p) | in |
Reflect.deleteProperty(o, p) | Delete |
Reflect.ownKeys(o) | All keys |
Reflect.getOwnPropertyDescriptor(o, p) | Descriptor |
Reflect.defineProperty(o, p, d) | Define |
Reflect.getPrototypeOf(o) | Get proto |
Reflect.setPrototypeOf(o, p) | Set proto |
Reflect.isExtensible(o) | Check |
Reflect.preventExtensions(o) | Lock |
Reflect.apply(fn, t, a) | Call |
Reflect.construct(C, a) | New |
Proxy vs Alternatives
| Feature | defineProperty | Proxy |
|---|---|---|
| Per-property | ✅ | ✅ |
| Whole object | ❌ | ✅ |
| Function calls | ❌ | ✅ |
in operator | ❌ | ✅ |
delete | ❌ | ✅ |
ownKeys | ❌ | ✅ |
| Performance | Faster | Slower |
Common Patterns
| Pattern | Trap |
|---|---|
| Validation | set |
| Defaults | get |
| Logging | get, set |
| Read-only | set throws |
| Hiding | ownKeys, has |
| Mocking | apply |
| Negative indices | get |
| Observability | get, set |
| Lazy loading | get |
Traps and their Reflect equivalents
| Trap | Reflect |
|---|---|
get | Reflect.get |
set | Reflect.set |
has | Reflect.has |
deleteProperty | Reflect.deleteProperty |
ownKeys | Reflect.ownKeys |
apply | Reflect.apply |
construct | Reflect.construct |
Best Practices
✅ Do This:
// Use Reflect inside traps
get(target, prop, receiver) {
return Reflect.get(target, prop, receiver); // ✅
}
// Validate writes
set(target, prop, value) {
if (!isValid(value)) throw new TypeError();
return Reflect.set(target, prop, value); // ✅
}
// Provide default forwarding
const proxy = new Proxy(target, {}); // ✅
// Use revocable when you need cleanup
const { proxy, revoke } = Proxy.revocable(target, handler); // ✅
// Return true from set to allow writes
set() { return true; } // ✅
// Use receiver for inheritance-safe access
Reflect.get(target, prop, receiver); // ✅
// Keep traps focused
get(target, prop) { log(prop); return target[prop]; } // ✅
❌ Don’t Do This:
// Don't forget to return from set
set(target, prop, value) {
target[prop] = value; // ❌ returns undefined
return true; // ✅
}
// Don't violate invariants
get(target, prop) {
return 'always'; // ⚠️ may break
}
// Don't skip non-configurable keys in ownKeys
ownKeys() {
return []; // ❌ if non-configurable exist
}
// Don't use Proxy in hot loops
for (let i = 0; i < 1e6; i++) proxy.x; // ❌ slow
// Use a plain object instead
// Don't wrap primitives
new Proxy(42, {}); // ❌ TypeError
// Don't forget to handle receiver
get(target, prop) {
return target[prop]; // ⚠️ breaks inherited getters
}
Reflect.get(target, prop, receiver); // ✅
// Don't proxy for simple cases
// If Object.defineProperty or a getter works, use that
Common Pitfalls
| Pitfall | Problem | Solution |
|---|---|---|
Forgetting return true in set | Silent failure | Return true |
Not using Reflect | Breaks invariants | Use Reflect.get/set |
| Proxy in hot loops | Slow | Avoid or use plain |
| Revoked proxy access | TypeError | Guard with try |
| Missing non-configurable keys | TypeError | Include them |
| Traps don’t forward | Behavior breaks | Use Reflect |
=== compares proxy | Not target | Compare .target if needed |
Losing this in methods | Wrong receiver | Use Reflect.get(..., receiver) |
Real-World Examples
1. Basic Get
const p = new Proxy({ name: 'Alice' }, {
get(t, prop) { return t[prop]; }
});
console.log(p.name);
// [ 'Alice' ]
2. Basic Set
const p = new Proxy({}, {
set(t, prop, v) { t[prop] = v; return true; }
});
p.x = 1;
console.log(p.x);
// [ 1 ]
3. Has
const p = new Proxy({ a: 1 }, {
has(t, prop) { return prop in t; }
});
console.log('a' in p);
// [ true ]
4. Delete
const p = new Proxy({ a: 1 }, {
deleteProperty(t, prop) { return Reflect.deleteProperty(t, prop); }
});
delete p.a;
console.log('a' in p);
// [ false ]
5. Validation
const strict = new Proxy({}, {
set(t, prop, v) {
if (prop === 'age' && typeof v !== 'number') throw new TypeError();
return Reflect.set(t, prop, v);
}
});
6. Default Values
const d = new Proxy({}, {
get(t, prop) { return t[prop] ?? 'default'; }
});
console.log(d.x);
// [ 'default' ]
7. Read-Only
const ro = new Proxy({ a: 1 }, {
set() { throw new Error('read-only'); }
});
8. Logging
const log = new Proxy({ name: 'Alice' }, {
get(t, prop) { console.log(`get ${String(prop)}`); return t[prop]; }
});
log.name;
// [ get name ]
9. Apply
const fn = new Proxy((a, b) => a + b, {
apply(t, thisArg, args) { return Reflect.apply(t, thisArg, args); }
});
10. Construct
class Person {}
const Ctor = new Proxy(Person, {
construct(t, args) { return Reflect.construct(t, args); }
});
11. Observable
const state = new Proxy({ count: 0 }, {
set(t, prop, v) {
console.log(`${String(prop)}: ${t[prop]} → ${v}`);
return Reflect.set(t, prop, v);
}
});
state.count = 1;
// [ count: 0 → 1 ]
12. Revocable
const { proxy, revoke } = Proxy.revocable({ a: 1 }, {});
revoke();
// proxy.a throws
13. Negative Index
const arr = new Proxy([1, 2, 3], {
get(t, prop) {
if (typeof prop === 'string' && /^-\d+$/.test(prop)) {
return t[t.length + Number(prop)];
}
return Reflect.get(t, prop);
}
});
console.log(arr[-1]);
// [ 3 ]
14. Hide Keys
const hidden = new Set(['secret']);
const p = new Proxy({ a: 1, secret: 2 }, {
ownKeys(t) { return Reflect.ownKeys(t).filter(k => !hidden.has(k)); },
getOwnPropertyDescriptor(t, prop) {
if (hidden.has(prop)) return undefined;
return Reflect.getOwnPropertyDescriptor(t, prop);
}
});
console.log(Object.keys(p));
// [ [ 'a' ] ]
15. Function Mocking
const calls = [];
const mock = new Proxy(fn, {
apply(t, thisArg, args) {
calls.push(args);
return Reflect.apply(t, thisArg, args);
}
});
16. Reflect.get
const o = { a: 1 };
console.log(Reflect.get(o, 'a'));
// [ 1 ]
17. Reflect.has
console.log(Reflect.has({ a: 1 }, 'a'));
// [ true ]
18. Reflect.ownKeys
console.log(Reflect.ownKeys({ a: 1, b: 2 }));
// [ [ 'a', 'b' ] ]
19. Reflect.apply
function f(x) { return this.v + x; }
console.log(Reflect.apply(f, { v: 10 }, [5]));
// [ 15 ]
20. Full Script
const target58 = { name: 'Alice', age: 30 };
const proxy58 = new Proxy(target58, {
get(obj, prop) {
console.log(`Getting ${String(prop)}`);
return obj[prop];
},
set(obj, prop, value) {
console.log(`Setting ${String(prop)} to ${value}`);
obj[prop] = value;
return true;
}
});
console.log(proxy58.name);
proxy58.age = 31;
console.log(target58.age);
const proxy58b = new Proxy(target58, {
has(obj, prop) { return prop in obj; },
deleteProperty(obj, prop) { return Reflect.deleteProperty(obj, prop); }
});
console.log('name' in proxy58b);
delete proxy58b.age;
console.log('age' in proxy58b);
const validator58 = new Proxy({}, {
set(obj, prop, value) {
if (prop === 'age' && typeof value !== 'number') {
throw new TypeError('Age must be a number');
}
obj[prop] = value;
return true;
}
});
validator58.age = 30;
console.log(validator58.age);
const defaults58 = new Proxy({}, {
get(obj, prop) {
return obj[prop] ?? `default-${String(prop)}`;
}
});
console.log(defaults58.name);
console.log(defaults58.foo);
const reflectTest58 = { name: 'Alice' };
console.log(Reflect.get(reflectTest58, 'name'));
console.log(Reflect.has(reflectTest58, 'name'));
Reflect.set(reflectTest58, 'age', 30);
console.log(reflectTest58.age);
console.log(Reflect.ownKeys(reflectTest58));
Visual: How a Proxy Works
┌──────────────────────────────────────────────┐
│ You: proxy.name │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────┐ │
│ │ Proxy Handler │ │
│ │ │ │
│ │ get(target, 'name') │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ your code runs │ │
│ │ │ │ │
│ │ └──► return value │ │
│ └──────────────┬──────────────────────┘ │
│ │ │
│ ▼ │
│ Result │
│ │
│ Target is only touched if the handler │
│ forwards to it (usually via Reflect) │
│ │
└──────────────────────────────────────────────┘
Visual: Proxy vs Target
┌──────────────────────────────────────────────┐
│ target = { name: 'Alice' } │
│ │
│ proxy = new Proxy(target, handler) │
│ │
│ proxy ─────► handler ─────► target │
│ │
│ Operations on proxy go through handler │
│ Operations on target stay direct │
│ │
│ proxy !== target │
│ │
└──────────────────────────────────────────────┘
Visual: Reflect in Traps
┌──────────────────────────────────────────────┐
│ const handler = { │
│ get(target, prop, receiver) { │
│ // custom logic │
│ return Reflect.get(target, prop, recv); │
│ } │
│ }; │
│ │
│ Reflect.get = the default behavior │
│ Use it to forward when you don't │
│ want to override │
│ │
└──────────────────────────────────────────────┘
Summary
| Concept | Syntax | Purpose |
|---|---|---|
| Proxy | new Proxy(target, handler) | Intercept operations |
| Revocable | Proxy.revocable(t, h) | Disable later |
get | get(target, prop, receiver) | Read |
set | set(target, prop, value, receiver) | Write |
has | has(target, prop) | in |
deleteProperty | deleteProperty(t, p) | delete |
ownKeys | ownKeys(target) | Key enumeration |
apply | apply(target, thisArg, args) | Function call |
construct | construct(target, args) | new |
| Reflect.get | Reflect.get(o, p) | Read default |
| Reflect.set | Reflect.set(o, p, v) | Write default |
| Reflect.has | Reflect.has(o, p) | in default |
| Reflect.ownKeys | Reflect.ownKeys(o) | Keys default |
| Reflect.apply | Reflect.apply(fn, t, a) | Call default |
| Reflect.construct | Reflect.construct(C, a) | New default |
Key takeaways:
- Proxy wraps an object and intercepts its operations via traps
- The target object is unchanged — the proxy is a separate wrapper
getandsetare the most-used traps — for reading and writinghas,deleteProperty,ownKeysinterceptin,delete, and enumerationapplyandconstructwork on function proxies- Reflect provides the default behavior for every trap — use it inside handlers
Reflect.get(target, prop, receiver)is the correct way to forward- Use
Proxy.revocablewhen you need to disable the proxy later - Common uses: validation, defaults, observability, read-only, logging, frameworks
- Watch performance — proxies are slower than plain objects
- Respect invariants — non-configurable properties must be reported correctly
- Reflect is not just for proxies — it’s a clean function API for object operations
Remember: Proxy and Reflect are the metaprogramming foundation of modern JavaScript. Proxy intercepts; Reflect forwards. Together they let you build objects that behave in custom ways — validating writes, providing defaults, tracking reads, mocking functions, hiding keys. Vue 3, MobX, and many libraries depend on them. Use them thoughtfully — they’re powerful but slower, and invariants must be respected. Master Proxy and Reflect, and you can shape how objects behave in ways ordinary JavaScript can’t.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!