JAVASCRIPT / MAPS, SETS, SYMBOLS, AND PROXIES
Reflect methods behind proxy handlers
Forward proxy traps through the matching Reflect method, passing receiver and newTarget, so getters, setters, subclassing, and invariants keep working.
What you will learn
- Forward any trap by calling the same-named Reflect function with the same arguments
- Pass receiver to Reflect.get/set so accessors see the proxy as this
- Return Reflect.set's boolean unchanged; false throws TypeError in strict mode
- Pass newTarget to Reflect.construct so subclass instances get the right prototype
Understanding Reflect methods behind proxy handlers
Each of the thirteen proxy traps has a Reflect function with the identical name and the identical parameter list, and that function performs exactly the internal operation the trap intercepted. That symmetry is the point of the API: a handler that wants to observe rather than change behaviour can finish with return Reflect.get(...arguments) and be sure nothing shifted. The older equivalents do not line up, which is why Reflect had to exist: delete is an operator rather than a function, Object.keys drops symbols and non-enumerable keys, and Object.defineProperty throws where a trap needs a boolean.
The receiver parameter is the object the operation actually started on, and it is not always the target. Reflect.get(target, key, receiver) looks the property up on target but binds this to receiver if the property turns out to be an accessor. Forward it and a getter on the target sees the proxy, so a read like this.other re-enters your trap; drop it and the object silently unwraps itself at the first accessor, and a proxy installed as someone's prototype ends up reading and writing the wrong object.
Reflect's second job is reporting failure as a boolean. Reflect.set, Reflect.defineProperty, Reflect.deleteProperty, Reflect.preventExtensions, and Reflect.setPrototypeOf return false where the Object counterpart throws, which is exactly the shape each trap must return. The engine then audits your answer against the real target: claiming a set succeeded on a non-writable, non-configurable property, or hiding such a key from ownKeys, is a TypeError. Forwarding to Reflect with the same target is the cheapest way to never fail that audit.
const person = {
first: 'Ada',
last: 'Lovelace',
get full() {
return `${this.first} ${this.last}`;
}
};
const forwarded = new Proxy(person, {
get(target, key, receiver) {
console.log('forwarded ->', String(key));
return Reflect.get(target, key, receiver);
}
});
const naive = new Proxy(person, {
get(target, key) {
console.log('naive ->', String(key));
return target[key];
}
});
console.log(forwarded.full);
console.log(naive.full);Every proxy trap has a same-named Reflect function taking the same arguments, so forwarding through it — receiver and newTarget included — is what makes a trap behave like the operation it replaced.
Worked examples
The boolean a set trap owes the engine
Shows that Reflect.set reports success as a boolean and that a false result becomes a TypeError under strict mode.
const counter = new Proxy({ count: 0 }, {
set(target, key, value, receiver) {
if (key === 'count') return false;
return Reflect.set(target, key, value, receiver);
}
});
console.log(Reflect.set(counter, 'label', 'hits'));
console.log(Reflect.set(counter, 'count', 5));
console.log(counter.label, counter.count);
function strictWrite() {
'use strict';
counter.count = 9;
}
try {
strictWrite();
} catch (err) {
console.log(err.name);
}Example explained
Line 1Reflect.set(counter, ...) runs the proxy's [[Set]], so the trap fires — Reflect is not a back door around a proxy.
Line 2The label branch forwards with the receiver, the value is stored on the target, and Reflect.set answers true.
Line 3The count branch returns false, so Reflect.set returns false and nothing is written; count stays 0.
Line 4Inside a strict-mode function an assignment whose [[Set]] returned false throws TypeError instead of failing quietly.
Reflect.ownKeys keeps symbols in the list
Filters underscore-prefixed keys out of a proxy while keeping symbol keys, and shows which traps each reflection API touches.
const secret = Symbol('secret');
const data = { id: 1, _internal: true, [secret]: 'hidden' };
const clean = new Proxy(data, {
ownKeys(target) {
return Reflect.ownKeys(target).filter(k => typeof k !== 'string' || k[0] !== '_');
},
getOwnPropertyDescriptor(target, key) {
if (typeof key === 'string' && key[0] === '_') return undefined;
return Reflect.getOwnPropertyDescriptor(target, key);
}
});
console.log(Reflect.ownKeys(clean).map(String).join(' | '));
console.log(Object.keys(clean).join(' | '));
console.log('_internal' in clean);Example explained
Line 1Reflect.ownKeys is the only built-in returning own string keys and own symbol keys in one array, which is what the ownKeys trap must produce.
Line 2The symbol survives the filter because the guard only rejects strings whose first character is an underscore.
Line 3Object.keys calls the ownKeys trap and then asks getOwnPropertyDescriptor about each string key, so hiding a key needs both traps.
Line 4'_internal' in clean is still true: the in operator uses the has trap, which is absent here and falls back to the target.
newTarget in a construct trap
Demonstrates that forwarding newTarget to Reflect.construct is what lets a subclass of a proxied class get the correct prototype.
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
}
const Traced = new Proxy(Point, {
construct(target, args, newTarget) {
console.log('new', target.name, 'as', newTarget.name, args.join(','));
return Reflect.construct(target, args, newTarget);
}
});
class Point3D extends Traced {
constructor(x, y, z) {
super(x, y);
this.z = z;
}
}
const p = new Point3D(1, 2, 3);
console.log(p instanceof Point3D, p instanceof Point);
console.log(Object.keys(p).join(','));Example explained
Line 1super(x, y) constructs through the proxy, so the trap runs with target Point, args [1, 2], and newTarget Point3D.
Line 2Passing newTarget on to Reflect.construct builds the instance from Point3D.prototype, so both instanceof checks pass.
Line 3Omitting that third argument would create the object from Point.prototype and p instanceof Point3D would be false.
Line 4A construct trap must return an object; Reflect.construct always does, so forwarding satisfies that rule for free.
Important notes
Reflect is a namespace object, not a class: there is no Reflect.prototype and new Reflect() throws.
Reflect does not bypass proxies. Hand a proxy to Reflect.get and that proxy's trap fires; when you omit receiver it simply defaults to the object you passed.
Common mistakes
Writing return target[key] in a get trap: accessors then run with this bound to the raw target, so every read they perform escapes the proxy and your logging or validation stops one level deep.
Writing set(target, key, value) { target[key] = value; } with no return: the trap answers undefined, so the write lands but [[Set]] reports failure and a strict-mode assignment throws TypeError.
Writing ownKeys(target) { return Object.keys(target); }: symbol keys and non-enumerable keys disappear, and if any omitted key is non-configurable the engine rejects the trap result with a TypeError.
Try it yourself
Change, predict, then run
Wrap an object with a get area() { return this.width * this.height; } accessor in a proxy whose get trap pushes each key into an array before returning Reflect.get(target, key, receiver), read .area once and log the array, then swap the forward for target[key] and compare the two logs.
Open the JavaScript workspaceCheck your understanding
A get trap does return target[key]. For a plain data property it returns the same value Reflect.get(target, key, receiver) would. Where does the difference actually show up?
- Nowhere — the two forms are interchangeable and Reflect is only a stylistic preference
- Only for symbol keys, because bracket access cannot look up symbols
- When the property is an accessor or is inherited, because this inside it binds to the target rather than the proxy
- When the target is frozen, because bracket access throws in strict mode
Show answer
Bracket access has nowhere to put a receiver, so an accessor found on the target or its prototype runs with this === target and every read it performs skips the proxy entirely. The first option is tempting because plain data properties really do return identical values, which is exactly why the bug stays hidden until an accessor appears or the proxy is used as a prototype; symbol lookup via target[sym] works fine, so the second option is simply false.