A Proxy get trap can return either `target[key]` or `Reflect.get(target, key, receiver)`. What does that third argument of Reflect.get change, and in which situation do the two forms return different values?
answer
- third argument is the receiver
- matters only for accessors
- the object the read started from
- proxy sitting on a prototype chain
- Reflect.set carries it too, plus the boolean
basics
~20 sThe third argument of Reflect.get is the receiver: the value used as this when the property turns out to be a getter. Omitting it makes getters run against the proxy's target, so a proxy sitting on a prototype chain returns the prototype's data instead of the instance's.
solid answer
~50 s`Reflect.get(target, key, receiver)` looks the property up on `target` but, if the property resolves to an accessor, invokes the getter with `receiver` as `this`. `target[key]` cannot express that — it always uses `target` itself. The difference is invisible for plain data properties and for a proxy you access directly, and it bites the moment the proxy sits on a prototype chain: reading `child.id` where `child`'s prototype is the proxy fires the trap with `receiver === child`, and a getter defined on the target that reads `this._id` will read the *target's* `_id` instead of the child's if you forward with `target[key]`. Forwarding with `Reflect.get(target, key, receiver)` preserves the object the read actually started from. `Reflect.set` takes the same fourth argument for the mirror-image reason: it decides which object the setter sees as `this` and which object ends up holding a newly created own property.
code
javascript · 19 linesconst parent = {
_id: 'parent',
get id() { return this._id; }
};
const naive = new Proxy(parent, {
get(t, key) { return t[key]; }
});
const correct = new Proxy(parent, {
get(t, key, receiver) { return Reflect.get(t, key, receiver); }
});
const a = Object.create(naive);
a._id = 'a';
const b = Object.create(correct);
b._id = 'b';
console.log(a.id); // "parent" — receiver was dropped
console.log(b.id); // "b" — receiver forwardedgo deeper
Know that Reflect.get takes an optional third argument called the receiver, that it defaults to the target, and that a Proxy get trap is handed one to pass along.
Explain that the receiver becomes this inside a getter, and walk through a lookup that starts on a child object and resolves on a prototype, showing why the search object and the receiver diverge there.
Diagnose the silent failure: a proxy placed on a prototype chain whose trap forwards with target[key] returns the target's state for every accessor. Name the same defect on the set side, where the write lands on the target instead of shadowing on the instance.
Frame it as an API-contract problem: a reflective wrapper must be transparent to consumers you have not met, including ones who use it as a prototype. Argue for mechanical forwarding to Reflect as a reviewable rule rather than trusting each trap author to re-derive spec semantics.
## What a receiver is When the engine reads `obj.x`, the specification's `[[Get]]` internal method takes two arguments: the property key, and a **receiver**. The receiver is the object the lookup *started from*. Normally receiver and the object being searched are the same thing — until the property is not found and the search moves up the prototype chain. At that point the search object changes to the prototype, but the receiver stays fixed at the original object. That is what makes `this` inside an inherited getter refer to the instance rather than the prototype. ```js const proto = { _id: 'proto', get id() { return this._id; } }; const child = Object.create(proto); child._id = 'child'; child.id; // "child" — the getter lives on proto, but this is child ``` ## Where a proxy enters the picture A `get` trap receives three parameters: `(target, key, receiver)`. That third one is the same receiver the engine threaded through the lookup — and the proxy hands it to you precisely so that you can pass it on. `Reflect.get`'s third argument is the slot it goes into. If you forward with `target[key]`, you have thrown the receiver away. `target[key]` is itself a fresh `[[Get]]` whose receiver is `target`, so any getter runs with `this === target`. ## The case where it actually differs Make the proxy a prototype: ```js const parent = { _id: 'parent', get id() { return this._id; } }; const naive = new Proxy(parent, { get(t, key) { return t[key]; } // receiver dropped }); const correct = new Proxy(parent, { get(t, key, receiver) { return Reflect.get(t, key, receiver); } }); const a = Object.create(naive); a._id = 'a'; const b = Object.create(correct); b._id = 'b'; a.id; // "parent" <-- the bug b.id; // "b" <-- correct ``` Reading `a.id` finds nothing own on `a`, walks to its prototype (the proxy) with receiver `a`, fires the trap, and the trap evaluates `parent['id']` — running the getter with `this === parent`. The instance's own `_id` is never consulted. This is one of the classic proxy bugs, and it is silent: nothing throws, you just get stale or shared data. A second, subtler variant: the getter may itself read another property, and every one of those reads goes through whichever object became `this`. Dropping the receiver therefore takes an *entire object graph* of reads off the instance and onto the target. ## The mirror case: Reflect.set `Reflect.set(target, key, value, receiver)` takes the same argument in the fourth position, and it does two things there. If the resolved property is an accessor, the setter runs with `receiver` as `this`. If it is a data property — or absent — the new own property is created **on the receiver**, not on the target. That is exactly the assignment semantics people expect: writing to an inherited data property does not modify the prototype, it shadows it on the instance. A `set` trap that forwards with `target[key] = value` silently writes through to the target and mutates shared state. ```js const handler = { set(t, key, value, receiver) { return Reflect.set(t, key, value, receiver); // returns a boolean, as the trap requires } }; ``` Note the return: `Reflect.set` yields a boolean, which is precisely what the trap must return. Forwarding with an assignment expression gives you the assigned value instead, and a falsy value there is read as "the set failed", producing a `TypeError` in strict-mode code. So `Reflect` fixes the return protocol at the same time it fixes the receiver. ## What the receiver defaults to Omit the argument and it defaults to `target`: `Reflect.get(obj, 'x')` behaves like `obj.x`. That default is why forgetting it is so easy to miss — everything works until a proxy ends up on a prototype chain, or until something else forwards a receiver into your trap. ## Practical rule Inside any trap, forward the parameters you were given, unchanged, to the same-named `Reflect` method. Do not reconstruct the operation by hand. The receiver is information you cannot recover once you drop it, and dropping it produces the kind of bug that only appears once a consumer starts using inheritance.
- If the property is a plain data property, does passing the receiver change anything for Reflect.get?No. For a data property the value is returned as stored, and the receiver is never consulted. It only matters when the resolved property is an accessor, because then the receiver becomes the getter's `this`. That is why the bug hides so well — it appears only once someone converts a field into a getter.
- Why does Reflect.set's receiver argument matter even when there is no setter involved?Because for a data property the write is performed on the receiver, not on the searched object. That is what makes assignment shadow an inherited property on the instance instead of mutating the prototype. Forwarding without the receiver writes straight through to the target, so every instance sharing that target sees the change.
- How would you notice this bug in review, given nothing throws?Look for any trap that reconstructs the operation with syntax — `target[key]`, `target[key] = value`, `delete target[key]` — instead of forwarding to the same-named Reflect call. If the trap signature declares a `receiver` parameter that the body never uses, that is the tell.
Think of the receiver as the "who asked" of a property read. The getter's job is to describe the object the request came from, not the object whose code happens to be running.
saying these in an interview costs you the question
- target[key] and Reflect.get(target, key, receiver) are always equivalent
- The receiver is the Proxy handler object
- The receiver only matters for performance
- Assignment in a set trap is fine because it writes the same value
- Getters always run with this bound to where they are defined