Course outline · 0% complete

0/27 lessons0%

Course overview →

Inheritance: extends and super

lesson 3-3 · ~11 min · 8/27

extends links two prototypes

class Dog extends Animal chains the prototypes, so Dog.prototype's own prototype becomes Animal.prototype.

A lookup on a dog now walks the instance, then Dog.prototype, then Animal.prototype, then Object.prototype. It is the same chain as lesson 3-1, just one link longer.

Two keywords come with it.

  • super(...) inside the constructor calls the parent constructor. A subclass constructor must call it before touching this.
  • super.method() inside a method calls the parent's version, which is useful when you override a method and still want the original behavior.

Statics are inherited too, and that is a detail people forget. extends also links Dog itself to Animal, so a static on the parent is callable as Dog.someStatic().

You will meet extends constantly even if you rarely write it, in class HttpError extends Error, in framework component classes, and in test doubles.

Interviewers use it to check that you can trace the longer lookup chain and recite the super rules that come with it.

Overriding and still calling the parent

The subclass extends the behavior rather than replacing it.

class Animal {
  constructor(name) {
    this.name = name;
  }
  speak() {
    return this.name + " makes a sound";
  }
}

class Dog extends Animal {
  speak() {
    return super.speak() + " (woof!)";
  }
}

const rex = new Dog("Rex");
console.log(rex.speak());
console.log(rex instanceof Dog);
console.log(rex instanceof Animal);

Output

Rex makes a sound (woof!)
true
true

Dog overrides speak and reuses the parent's version through super.speak.

instanceof is true for both classes, because both prototypes are on rex's chain.

Dog declares no constructor, which is legal. It inherits a default one that forwards every argument to super, so new Dog("Rex") still sets this.name.

The override wins because of lookup order. rex.speak is found on Dog.prototype first, and the parent's version is only reachable through super.

Inside super.speak() the receiver is still rex, so this.name reads "Rex". super changes where the function is found, never what this points to.

Writing this.speak() in place of super.speak() would find Dog.prototype.speak again and recurse until the stack overflows, which is a mistake worth recognizing on sight.

rex (instance)Puppy.prototypespeak()Dog.prototypesuper.speak()Object.prototypefoundone step up
extends links the prototypes, and super.speak() skips one link up to the parent version of the method.

It walks rex's prototype chain looking for Animal.prototype.

instanceof asks one question. Does the right-hand side's .prototype appear anywhere on this object's prototype chain?

For rex the chain is Dog.prototype, then Animal.prototype, then Object.prototype, so the answer is true.

Names and property shapes are irrelevant. An unrelated object with an identical name and an identical speak method is not an instance of Animal, because the chain is what counts.

The same logic makes rex instanceof Object true, since almost every chain ends there.

One practical failure is worth knowing. Two copies of the same library loaded separately produce two distinct Animal.prototype objects, so an object built by one copy fails instanceof against the other. That is why some libraries check a marker property instead.

It throws a ReferenceError, because this is unavailable until super() has run.

The parent constructor is the code that creates and initializes the object, so JavaScript refuses to hand you this until it has finished. Touching this first throws rather than silently misbehaving.

The rule covers reads as well as writes, so even console.log(this) before super() fails.

The fix is ordering rather than restructuring. Call super(name) first, then run whatever subclass-specific assignments you need.

Omitting the constructor entirely is often the better answer. The subclass then gets a default constructor that forwards all arguments to super automatically, and there is nothing to get wrong.

Forgetting super() altogether in a constructor you did write fails the same way, with a ReferenceError as soon as the constructor returns without having called it.

Interview note: prefer shallow hierarchies

Inheritance is a sharp tool.

One level, as in Dog extends Animal, is fine and reads well. Deep trees get brittle, because a change in a distant ancestor can break a subclass that never mentions it.

The specific failure has a name. Behavior gets scattered across levels, so answering "what does this method do" means reading four files, and every subclass silently depends on parent internals.

Interviewers like hearing that you know the alternative. Composition builds objects out of smaller pieces, in the spirit of the module pattern from lesson 1-3, instead of inheriting from a chain of ancestors.

In practice composition means holding a collaborator rather than becoming one. A Logger passed into a service is composition, and extends Logger is inheritance, and the first one can be swapped at runtime.

"Favor composition over inheritance" is a safe, correct instinct to voice, and the honest version adds one caveat. When the runtime demands a real subclass, as extends Error does for stack traces and instanceof checks, inheritance is the right answer.

Square

A one-argument constructor feeding a two-argument parent.

class Rectangle {
  constructor(width, height) {
    this.width = width;
    this.height = height;
  }
  area() {
    return this.width * this.height;
  }
}

class Square extends Rectangle {
  constructor(side) {
    super(side, side);
  }
}

const s = new Square(6);
console.log(s.area());
console.log(s instanceof Rectangle);

Output

36
true

Square's constructor calls super(side, side), and no other code is needed.

This is the case where a constructor is genuinely required, because the argument counts differ. A default constructor would forward one argument and leave height as undefined, making the area NaN.

area is not redefined anywhere in Square, and it works because the lookup walks up to Rectangle.prototype.

Nothing keeps the invariant afterward, which is the honest caveat. Assigning s.width = 10 leaves a "square" with unequal sides, and enforcing it would take getters and setters.

That gap is a famous argument against modeling this relationship with inheritance at all, since a square is only substitutable for a rectangle when neither one is mutable.