skip to content

In JavaScript, when a method body contains `super.greet()`, how does the engine decide which object `greet` is looked up on, and why does copying that method onto a different object not change the answer?

level: seniorimportance: should knowfreq 40%

answer

  1. lookup and receiver come from different places
  2. the method remembers where it was written
  3. only concise definitions get the slot
  4. copying the function copies the link
  5. a three-level chain would loop otherwise

basics

~20 s

Methods written with the shorthand syntax carry a hidden link to the object they were defined in, and super looks the name up on that object's prototype — not on the receiver. Copying the method elsewhere keeps the original link.

solid answer

~40 s

A method defined with the concise syntax — in a class body or an object literal — carries an internal slot, `[[HomeObject]]`, pointing at the object it was written in. `super.greet` is resolved by taking the prototype of that home object and looking `greet` up there; the found function is then called with the current `this`. Crucially, the *lookup* uses `[[HomeObject]]` while the *receiver* is `this`, so the two can diverge. Assigning the method onto an unrelated object with `Object.assign` or a plain assignment copies the function object, slot and all, so `super` still reaches the original parent even though `this` is now the new object. A method written as `key: function () {}` has no home object at all, and `super` inside it is a `SyntaxError`.

code

javascript · 10 lines
javascript
const parent = { greet() { return 'parent'; } };
const child = Object.setPrototypeOf(
  { greet() { return 'child -> ' + super.greet(); } },
  parent
);
const other = Object.setPrototypeOf({}, { greet() { return 'other'; } });
other.greet = child.greet;

console.log(child.greet()); // child -> parent
console.log(other.greet()); // child -> parent

go deeper

for a junior

Know that super.someMethod() calls the parent class's version of a method while this still refers to the current instance.

for a middle

Explain that concise method definitions carry a hidden home-object link, that super looks up from that object's prototype, and that a function expression assigned to a property cannot use super at all.

for a senior

Show the divergence in a real scenario — a copied or mixed-in method whose super still points at the donor's parent — and explain why lookup via the receiver would recurse in a three-level hierarchy.

for a principal

Weigh mixin strategies on this basis: class-factory mixins compose correctly through super, while property-copying helpers quietly break it, which decides how you let library consumers extend your types.

## Two different questions `super.x` has to answer A call like `super.greet()` involves two decisions: *where do I look the property up*, and *what is `this` when I call it*. JavaScript answers them from different sources, and every surprise in this area comes from confusing the two. - **Lookup** starts from the prototype of the method's `[[HomeObject]]`. - **Receiver** is the current `this`, unchanged. That split is deliberate. Using `this` for lookup would be the naive design, and it breaks immediately: a three-level hierarchy where the middle class calls `super.greet()` would find its own method again through `Object.getPrototypeOf(this)` and recurse forever. ## What `[[HomeObject]]` is When the engine evaluates a **concise method definition**, it stamps the resulting function with an internal slot recording the object the method was defined in. Concise means the shorthand form — `greet() {}`, `get x() {}`, `set x(v) {}`, `*gen() {}`, `async run() {}` — inside either a class body or an object literal. For an instance method of a class, the home object is `C.prototype`; for a static method it is the class `C` itself; for an object literal method it is the literal. A function *expression* assigned to a property gets no such slot: ```js const obj = { greet: function () { return super.greet(); } // SyntaxError }; ``` This is rejected at parse time rather than failing at call time, because the parser knows `super` cannot be resolved in that position. ## The consequence: `super` travels with the method Because the slot lives on the function object, it survives being moved: ```js const parent = { greet() { return 'parent'; } }; const child = Object.setPrototypeOf( { greet() { return 'child -> ' + super.greet(); } }, parent ); const other = Object.setPrototypeOf({}, { greet() { return 'other'; } }); other.greet = child.greet; child.greet(); // "child -> parent" other.greet(); // "child -> parent" <- not "child -> other" ``` `other.greet()` runs with `this === other`, so any `this.x` inside would read `other`'s state — but `super.greet` still resolves through `child`, the object the method was written in. `Object.assign(other, child)` behaves identically, because it copies the same function object. This matters for mixin helpers that copy methods between prototypes. Copying a method that uses `super` silently keeps pointing at the donor's parent, so the mixin appears to work until someone changes the donor's hierarchy. Mixins implemented as class factories — a function returning `class extends Base {}` — avoid the problem entirely, because each generated class body has its own home object at the right place in the chain. ## Static methods and getters The same rule applies with a different home object. Inside a static method of `Child`, the home object is `Child`, so `super.create()` looks `create` up on `Object.getPrototypeOf(Child)` — the parent class. Inside an accessor, `super.x` reads or writes through the parent's getter or setter with `this` still bound to the current instance, which is how a subclass accessor can wrap the parent's. ## Arrow functions and nested scopes An arrow function has no `[[HomeObject]]` of its own, and `super` inside it resolves lexically to the enclosing method's: ```js class Child extends Parent { greet() { return [1].map(() => super.greet()); // fine } } ``` A nested ordinary `function` expression is not a method definition, so `super` inside it is a `SyntaxError` even though it sits inside a class body. ## Reassigning prototypes at runtime Because lookup goes through the home object's prototype and that prototype is read at call time, changing it changes where `super` lands: ```js Object.setPrototypeOf(Child.prototype, OtherParent.prototype); ``` After this, every `super.x` in `Child`'s instance methods resolves against `OtherParent.prototype`. The home object is fixed; its prototype is not. This is occasionally used by test doubles and hot-reload machinery, and it is a good reason to treat `setPrototypeOf` on a live prototype as a heavy operation rather than a tweak. ## How to reason about it in an interview State the rule as a sentence: *lookup from the home object's prototype, call with the current `this`*. Then demonstrate the divergence with the copied-method example. That single example proves you understand the slot exists, that it is attached to the function rather than the call site, and why the naive `Object.getPrototypeOf(this)` design fails.

  • Why would resolving super through Object.getPrototypeOf(this) instead of the home object be broken?
    It infinite-loops in a three-level hierarchy. If `Middle.prototype.greet` calls `super.greet()` and `this` is a `Leaf` instance, `Object.getPrototypeOf(this)` is `Leaf.prototype`, whose lookup finds `Middle`'s own `greet` again. Anchoring to the home object makes the starting point depend on where the code was written, so each level moves strictly one step up.
  • Can super appear inside an arrow function or a nested function expression within a method?
    Inside an arrow function, yes — arrows have no home object of their own, so `super` resolves lexically to the enclosing method's, which makes `super.x` work inside callbacks like `map`. Inside a nested ordinary `function` expression, no: it is not a method definition, so `super` there is a `SyntaxError` even within a class body.
  • What breaks when a mixin helper copies methods that use super between prototypes?
    The copied methods keep the donor's `[[HomeObject]]`, so their `super` calls still resolve through the donor's parent rather than the recipient's. The result silently ignores the recipient's hierarchy and breaks the moment the donor's parent changes. Class-factory mixins — a function returning `class extends Base {}` — give each body a correctly placed home object instead.
  • Does changing a prototype at runtime affect where existing super calls land?
    Yes. The home object is fixed when the method is defined, but its prototype is read at call time, so `Object.setPrototypeOf(Child.prototype, Other.prototype)` redirects every `super.x` in `Child`'s methods to `Other.prototype`. That is one reason mutating a live prototype is a heavyweight, deoptimising operation rather than a small tweak.

saying these in an interview costs you the question

  • Says super resolves through the prototype of this
  • Thinks super rebinds when the method is copied
  • Believes super works in any function inside a class body
  • Claims super.greet() calls the parent with the parent as this
  • Assumes Object.assign-based mixins carry super correctly

context