Class inheritance

Inheritance lets one class build on another. You take a class that already does something useful, and you make a new class that keeps all of that behavior while adding or changing a few things. No copy-paste, no duplication — the child class points back at the parent and borrows whatever it doesn’t define itself.

That “points back at” is the whole story, and by the end of this article you’ll see exactly how the arrow is wired.

The “extends” keyword

Start with a class Vehicle:

class Vehicle {
  constructor(name) {
    this.speed = 0;
    this.name = name;
  }
  accelerate(speed) {
    this.speed = speed;
    alert(`${this.name} accelerates to ${this.speed}.`);
  }
  halt() {
    this.speed = 0;
    alert(`${this.name} comes to a halt.`);
  }
}

let vehicle = new Vehicle("My vehicle");

A vehicle object and the Vehicle class relate to each other like this: the object stores the data (name, speed), and the class stores the shared methods on Vehicle.prototype.

Vehicle.prototype
constructor: Vehicle
accelerate: function
halt: function
[[Prototype]]
vehicle
name: “My vehicle”
speed: 0
A Vehicle instance holds its own data; the shared methods live on Vehicle.prototype.

Now suppose you want a class Ebike. Ebikes are vehicles, so Ebike should sit on top of Vehicle and reuse everything a generic vehicle can do — accelerating, halting — while adding its own ebike tricks.

The syntax for that is class Child extends Parent:

class Ebike extends Vehicle {
  ringBell() {
    alert(`${this.name} rings its bell!`);
  }
}

let ebike = new Ebike("City Ebike");

ebike.accelerate(5); // City Ebike accelerates to 5.
ebike.ringBell(); // City Ebike rings its bell!

An Ebike instance can call both its own method ebike.ringBell() and the inherited ebike.accelerate(). The class only wrote ringBell, yet accelerate works anyway.

Try it live. The same ebike object answers to both methods — one it defines, one it inherits — and each writes its result into the log below:

interactiveInherited accelerate(), own ringBell()

What does extends actually do? Nothing magical — it wires up the same prototype chain you already know. It sets Ebike.prototype.[[Prototype]] to Vehicle.prototype. When a property isn’t found on Ebike.prototype, the engine follows that link up to Vehicle.prototype.

Vehicle.prototype
accelerate · halt · constructor
▲ [[Prototype]]  (3. found accelerate here)
Ebike.prototype
ringBell · constructor
▲ [[Prototype]]  (2. has ringBell, not accelerate)
ebike
name: “City Ebike” · speed: 5
(1. start here, no own accelerate)
extends links Ebike.prototype's [[Prototype]] to Vehicle.prototype. Lookups climb the chain from the bottom up.

So resolving ebike.accelerate walks the chain from the bottom:

  1. The ebike object itself — no accelerate.
  2. Its prototype, Ebike.prototype — has ringBell, but no accelerate.
  3. Its prototype, Vehicle.prototype — here’s accelerate. Done.

This is the same mechanism the language uses for its own built-ins. As covered in Native prototypes, Date.prototype.[[Prototype]] is Object.prototype, which is why every date can call generic object methods like toString. Your classes plug into that same chain.

Overriding a method

By default, any method a child doesn’t define is inherited from the parent verbatim. Define a method with the same name in the child, and the child’s version wins for instances of the child:

class Ebike extends Vehicle {
  halt() {
    // ...now this will be used for ebike.halt()
    // instead of halt() from class Vehicle
  }
}

Total replacement is rarely what you want, though. More often you want to keep the parent’s work and wrap something around it — run the original, then add a step, or prepare something first and then delegate. For that, classes give you super:

  • super.method(...) calls a method from the parent.
  • super(...) calls the parent constructor, and only from inside a constructor.

Here the ebike rings its bell automatically whenever it halts. Its halt calls the parent’s halt first, then adds ringBell:

class Vehicle {

  constructor(name) {
    this.speed = 0;
    this.name = name;
  }

  accelerate(speed) {
    this.speed = speed;
    alert(`${this.name} accelerates to ${this.speed}.`);
  }

  halt() {
    this.speed = 0;
    alert(`${this.name} comes to a halt.`);
  }

}

class Ebike extends Vehicle {
  ringBell() {
    alert(`${this.name} rings its bell!`);
  }

  halt() {
    super.halt(); // call parent halt
    this.ringBell(); // and then ring
  }
}

let ebike = new Ebike("City Ebike");

ebike.accelerate(5); // City Ebike accelerates to 5.
ebike.halt(); // City Ebike comes to a halt. City Ebike rings its bell!

Ebike.halt reuses the parent’s zero-the-speed logic through super.halt(), then layers on its own behavior. No copy-paste of the parent’s body.

Press halt below and watch two lines appear from a single call: the parent’s message first, then the ebike’s. Edit the code to remove super.halt() and re-run to see the parent step vanish:

interactivehalt() calls super.halt(), then rings

Overriding constructor

Constructors add a wrinkle.

So far, Ebike had no constructor of its own. When a class extends another and doesn’t declare a constructor, the engine generates an “empty” one for you. Per the specification, it looks like this:

class Ebike extends Vehicle {
  // generated for extending classes without own constructors
  constructor(...args) {
    super(...args);
  }
}

It forwards every argument straight to the parent constructor. That’s why new Ebike("City Ebike") worked earlier even though we never wrote a constructor.

Now write your own constructor — say Ebike needs a batteryRange alongside name:

class Vehicle {
  constructor(name) {
    this.speed = 0;
    this.name = name;
  }
  // ...
}

class Ebike extends Vehicle {

  constructor(name, batteryRange) {
    this.speed = 0;
    this.name = name;
    this.batteryRange = batteryRange;
  }

  // ...
}

// Doesn't work!
let ebike = new Ebike("City Ebike", 60); // Error: this is not defined.

An error, and ebikes can no longer be created. The rule behind it:

  • A constructor in an inheriting class must call super(...), and it must do so before touching this.

Why does the language insist on that? The reason is real, not arbitrary.

JavaScript treats the constructor of an inheriting class differently from an ordinary function. Such a constructor — a “derived constructor” — carries a hidden marker [[ConstructorKind]]: "derived". That marker changes what new does:

  • Run a regular function with new, and it creates a fresh empty object and binds it to this.
  • Run a derived constructor with new, and it does not create that object. It leaves the job to the parent constructor.
base constructor
[[ConstructorKind]]: “base”
new → creates this, then runs body
derived constructor
[[ConstructorKind]]: “derived”
new → this is empty until super() runs
Who creates the this object depends on [[ConstructorKind]].

So a derived constructor must call super() to let the parent build the object that becomes this. Skip that call, or reach for this before it, and this doesn’t exist yet — hence this is not defined.

Add the super(name) call first, and everything works:

class Vehicle {

  constructor(name) {
    this.speed = 0;
    this.name = name;
  }

  // ...
}

class Ebike extends Vehicle {

  constructor(name, batteryRange) {
    super(name);
    this.batteryRange = batteryRange;
  }

  // ...
}

// now fine
let ebike = new Ebike("City Ebike", 60);
alert(ebike.name); // City Ebike
alert(ebike.batteryRange); // 60

super(name) runs the Vehicle constructor, which sets up this with speed and name. Only then does Ebike add batteryRange.

Overriding class fields: a tricky note

You can override class fields, not just methods. But there’s a surprising rule when an overridden field is read inside the parent constructor — and it differs from what most languages do.

class Vehicle {
  kind = 'vehicle';

  constructor() {
    alert(this.kind); // (*)
  }
}

class Ebike extends Vehicle {
  kind = 'ebike';
}

new Vehicle(); // vehicle
new Ebike(); // vehicle

Ebike overrides kind with 'ebike'. It has no constructor, so the Vehicle constructor runs. Yet both new Vehicle() and new Ebike() print vehicle at line (*).

The parent constructor always sees its own field value, never the child’s override.

That feels wrong until you compare it with methods. Here’s the same shape, but kind is replaced by a showKind() method:

class Vehicle {
  showKind() {  // instead of this.kind = 'vehicle'
    alert('vehicle');
  }

  constructor() {
    this.showKind(); // instead of alert(this.kind);
  }
}

class Ebike extends Vehicle {
  showKind() {
    alert('ebike');
  }
}

new Vehicle(); // vehicle
new Ebike(); // ebike

Now the output splits: new Ebike() prints ebike. The parent constructor calls the overridden method. That’s the behavior you’d expect. So why do fields behave differently?

The answer is timing — when each field gets initialized:

  • For a base class (one that extends nothing), fields are set up right before the constructor body runs.
  • For a derived class, fields are set up immediately after super() returns.
1. new Ebike() → derived constructor starts (implicit) → calls super()
2. Vehicle fields set: kind = ‘vehicle’
3. Vehicle constructor body runs → alert(this.kind) → “vehicle”  (Ebike.kind not set yet)
4. super() returns → Ebike fields set: kind = ‘ebike’
Init order for new Ebike(): the parent constructor runs while Ebike's fields still don't exist.

Ebike is derived and has no explicit constructor, which is the same as constructor(...args) { super(...args); }. So new Ebike() calls super(), the Vehicle constructor runs to completion, and only after that do the Ebike fields get assigned. During the parent constructor there is no Ebike.kind yet, so Vehicle’s own kind is what’s read.

Methods dodge this because they don’t live on the instance at all — they’re on the prototype, resolved dynamically at call time, so the override is already reachable.

This split between fields and methods is specific to JavaScript. It only bites when an overridden field is consumed inside the parent constructor, which is uncommon. If it ever causes trouble, replace the field with a method or a getter/setter, and the override resolves normally.

Run both versions side by side. The parent constructor reads the value the exact same way in each — yet the field version ignores Ebike’s override while the method version honors it:

interactiveOverridden field vs overridden method, seen from the parent constructor

Super: internals, [[HomeObject]]

Time to look under the hood of super. There’s a genuinely interesting puzzle here.

Here’s the provocation: from everything covered so far, super shouldn’t be able to work at all.

Think about the mechanics. When an object method runs, it receives the current object as this. If that method calls super.method(), the engine needs the parent’s method — that is, a method from the prototype above the one where the current method is defined. Where does it get it?

The obvious guess: use this. The engine has this, so grab this.__proto__.method. That naive idea falls apart, as the next example shows. We’ll use plain objects instead of classes to keep it simple.

Here ebike.__proto__ = vehicle. Inside ebike.honk() we try to reach vehicle.honk() via this.__proto__:

let vehicle = {
  name: "Vehicle",
  honk() {
    alert(`${this.name} honks.`);
  }
};

let ebike = {
  __proto__: vehicle,
  name: "Ebike",
  honk() {
    // that's how super.honk() could presumably work
    this.__proto__.honk.call(this); // (*)
  }
};

ebike.honk(); // Ebike honks.

Line (*) pulls honk off the prototype (vehicle) and calls it with the current object as context. The .call(this) matters: a plain this.__proto__.honk() would run the parent honk with this set to the prototype rather than the real object. With two levels, this works — you get the right output.

Add a third object to the chain, though, and it collapses:

let vehicle = {
  name: "Vehicle",
  honk() {
    alert(`${this.name} honks.`);
  }
};

let ebike = {
  __proto__: vehicle,
  honk() {
    // ...bounce around ebike-style and call parent (vehicle) method
    this.__proto__.honk.call(this); // (*)
  }
};

let cargoBike = {
  __proto__: ebike,
  honk() {
    // ...do something cargo-style and call parent (ebike) method
    this.__proto__.honk.call(this); // (**)
  }
};

cargoBike.honk(); // Error: Maximum call stack size exceeded

cargoBike.honk() now blows the stack. Trace it and the reason surfaces. In both (*) and (**), this is the object the outermost call started with — cargoBike. That’s the fundamental rule: every method receives the current object as this, not the prototype it happens to live on.

So in both lines, this.__proto__ evaluates to the same thing: ebike. Neither line ever climbs past ebike.

vehicle.honk  (never reached)
proto
ebike.honk  ↺ calls itself
proto  this.proto is ebike for BOTH calls
cargoBike.honk  this = cargoBike
With this.__proto__, both honk() methods resolve to ebike.honk because this is always cargoBike — an infinite loop.

Walk it step by step:

  1. cargoBike.honk() runs line (**). It calls ebike.honk with this = cargoBike.
    // inside cargoBike.honk() we have this = cargoBike
    this.__proto__.honk.call(this) // (**)
    // becomes
    cargoBike.__proto__.honk.call(this)
    // that is
    ebike.honk.call(this);
  2. Inside ebike.honk, line (*) tries to go one level higher — but this is still cargoBike, so this.__proto__.honk is ebike.honk again.
    // inside ebike.honk() we also have this = cargoBike
    this.__proto__.honk.call(this) // (*)
    // becomes
    cargoBike.__proto__.honk.call(this)
    // or (again)
    ebike.honk.call(this);
  3. So ebike.honk calls ebike.honk forever. It can’t ascend, and the stack overflows.

this alone cannot solve this. The engine needs to know where the running method was defined, independent of what this currently is.

[[HomeObject]]

The fix is another hidden property on functions: [[HomeObject]].

When a function is written as a method inside a class or an object literal, its [[HomeObject]] is set to that class or object. super then reads [[HomeObject]], takes its prototype, and looks up the parent method there — no reliance on this at all.

Here’s the same three-level chain, now using real super:

let vehicle = {
  name: "Vehicle",
  honk() {         // vehicle.honk.[[HomeObject]] == vehicle
    alert(`${this.name} honks.`);
  }
};

let ebike = {
  __proto__: vehicle,
  name: "Ebike",
  honk() {         // ebike.honk.[[HomeObject]] == ebike
    super.honk();
  }
};

let cargoBike = {
  __proto__: ebike,
  name: "Cargo Ebike",
  honk() {         // cargoBike.honk.[[HomeObject]] == cargoBike
    super.honk();
  }
};

// works correctly
cargoBike.honk();  // Cargo Ebike honks.

Each method carries its own [[HomeObject]]. Inside cargoBike.honk, super.honk() looks at cargoBike’s prototype (ebike). Inside ebike.honk, super.honk() looks at ebike’s prototype (vehicle). The lookup steps up the chain correctly because it’s anchored to where each method lives, not to the ever-constant this.

Methods are not “free”

Functions in JavaScript are normally “free”: unbound to any object, copyable between objects, callable with whatever this you supply. [[HomeObject]] dents that principle. A method now remembers the object it was defined in, and that bond can’t be reassigned — [[HomeObject]] is fixed forever.

The only feature that reads [[HomeObject]] is super. So a method that never uses super is still effectively free — copy it around freely. A method that does use super can misbehave once moved, because it keeps resolving super relative to its original home.

Here’s what goes wrong:

let scooter = {
  announce() {
    alert(`I'm a scooter`);
  }
};

// ebike inherits from scooter
let ebike = {
  __proto__: scooter,
  announce() {
    super.announce();
  }
};

let boat = {
  announce() {
    alert("I'm a boat");
  }
};

// ferry inherits from boat
let ferry = {
  __proto__: boat,
  announce: ebike.announce // (*)
};

ferry.announce();  // I'm a scooter (?!?)

ferry.announce() announces “I’m a scooter” — clearly not what a ferry should say. Here’s the chain of cause:

  • Line (*) copied ebike’s announce onto ferry, perhaps to avoid duplicating code.
  • That method’s [[HomeObject]] is still ebike, frozen at the moment it was defined. Copying can’t change it.
  • Its body calls super.announce(), which climbs from ebike to scooter — completely ignoring that the method now lives on ferry, whose real chain leads to boat.

See the surprise for yourself. ferry sits on top of boat, yet asking the ferry to speak produces the scooter’s line, because the borrowed method’s [[HomeObject]] is still ebike:

interactiveA copied super-method keeps its old home
scooter
ebike  announce
[[HomeObject]] = ebike
boat
ferry  announce (copied)
super still points at scooter ✗
ferry.announce keeps ebike as its [[HomeObject]], so super resolves to scooter — not boat.

Methods, not function properties

[[HomeObject]] is set for methods in both classes and plain objects. But in an object literal it’s only set when you use the shorthand method syntax method(), not the assignment form method: function().

The distinction can look cosmetic, but the engine treats the two forms differently. Below, the assignment form leaves [[HomeObject]] unset, so super has nothing to resolve against and the call fails:

let vehicle = {
  honk: function() { // intentionally writing like this instead of honk() {...
    // ...
  }
};

let ebike = {
  __proto__: vehicle,
  honk: function() {
    super.honk();
  }
};

ebike.honk();  // Error calling super (because there's no [[HomeObject]])

The takeaway: if a method uses super, define it with method shorthand.

Summary

  1. Extend a class with class Child extends Parent:
    • This sets Child.prototype.__proto__ to Parent.prototype, so instances inherit the parent’s methods.
  2. Overriding a constructor:
    • The child constructor must call super() before it uses this, because a derived constructor relies on the parent to create the this object.
  3. Overriding another method:
    • Call the parent’s version with super.method() from inside the child’s method.
  4. Internals:
    • Each method remembers its class or object in [[HomeObject]]. That’s how super finds the parent method — via the home object’s prototype, not via this.
    • Because of that fixed bond, copying a super-using method to a different object can produce wrong results.

And a related point: arrow functions have no this or super of their own, so they transparently borrow both from the surrounding code.