JavaScript Inheritance

6 minute read

extends lets one class build on another — sharing fields, methods, and the prototype chain. Inheritance is powerful, but it’s also the most over-used pattern in OOP. This chapter shows the mechanics (super, overriding, the prototype chain) and when to prefer composition or mixins instead.

extends and super

class Animal {
  constructor(name) { this.name = name; }
  speak()           { return `${this.name} makes a noise.`; }
}

class Dog extends Animal {
  constructor(name, breed) {
    super(name);                   // must call before using `this`
    this.breed = breed;
  }

  speak() {                        // override
    return `${this.name} barks.`;
  }
}

const d = new Dog("Rex", "Lab");
console.log(d.speak());            // Output: Rex barks.
console.log(d instanceof Dog);     // Output: true
console.log(d instanceof Animal);  // Output: true

Two rules to remember:

  1. A subclass constructor must call super(...) before using this.
  2. super in a method calls the parent version.

Calling the Parent — super.method()

Common pattern: override but keep the parent behaviour.

class Logger {
  log(msg) { console.log(`[log] ${msg}`); }
}

class TimedLogger extends Logger {
  log(msg) {
    super.log(`${new Date().toISOString()} – ${msg}`);
  }
}

new TimedLogger().log("hello");
// Output: [log] 2026-…T…Z – hello

super.x works on any instance method or accessor — useful in toString, valueOf, lifecycle hooks, etc.

Static Inheritance

Static members are inherited too:

class A {
  static label = "A";
  static who()  { return this.label; }
}

class B extends A {
  static label = "B";              // override
}

A.who();                           // Output: "A"
B.who();                           // Output: "B"  — `this` is B inside static

this inside a static method is the class it’s called on. That’s why class-level factories chain correctly.

The Prototype Chain

A subclass instance has a two-link chain:

d → Dog.prototype → Animal.prototype → Object.prototype → null

Property lookup walks the chain until it finds the name (or hits null):

class Animal { speak() {} }
class Dog extends Animal { bark() {} }
const d = new Dog();

Object.getPrototypeOf(d) === Dog.prototype;                          // true
Object.getPrototypeOf(Dog.prototype) === Animal.prototype;           // true

This is why method overriding “just works” — the subclass version is found first.

Overriding Fields vs Methods

class Base   { type = "base"; describe() { return `I am ${this.type}`; } }
class Child  extends Base { type = "child"; }

const c = new Child();
console.log(c.describe());         // Output: I am child

Field initialisers in the child run after super(), so they shadow parent fields. Methods on the prototype are simply replaced.

Abstract-ish Patterns

JavaScript has no abstract keyword, but you can simulate one:

class Shape {
  constructor() {
    if (new.target === Shape)
      throw new Error("Shape is abstract — subclass it");
  }
  area() { throw new Error("Subclass must implement area()"); }
}

class Circle extends Shape {
  constructor(r) { super(); this.r = r; }
  area() { return Math.PI * this.r ** 2; }
}

new Shape();                       // ❌ throws
new Circle(3).area();              // 28.27…

new.target is the constructor invoked with new — handy guard for “don’t instantiate this directly”.

Composition Often Beats Inheritance

Inheritance forces an “is-a” relationship and a fixed hierarchy. Most real problems are “has-a”.

// ❌ Deep, brittle hierarchy
class Animal {}
class Bird extends Animal { fly() {} }
class Fish extends Animal { swim() {} }
class Duck extends Bird   { swim() {} }   // duplicate swim logic

// ✅ Compose capabilities
const canFly  = (self) => ({ fly()  { /* … */ } });
const canSwim = (self) => ({ swim() { /* … */ } });

function makeDuck(name) {
  const self = { name };
  return Object.assign(self, canFly(self), canSwim(self));
}
Style Best for Pain points
Inheritance (extends) Clear “is-a” with stable spec — DOM events, React.Component-style Diamond problems, deep chains, tight coupling
Composition Mix-and-match capabilities Slightly more boilerplate up front

A widely quoted guideline: prefer composition over inheritance, especially past one or two levels deep.

Mixins — Reusable Capability Bundles

When you really do need to share behaviour across unrelated classes:

const Serializable = (Base) => class extends Base {
  toJSON() {
    return JSON.stringify({ ...this, type: this.constructor.name });
  }
};

const Comparable = (Base) => class extends Base {
  equals(other) {
    return JSON.stringify(this) === JSON.stringify(other);
  }
};

class Point {}
class SmartPoint extends Comparable(Serializable(Point)) {
  constructor(x, y) { super(); this.x = x; this.y = y; }
}

const p = new SmartPoint(1, 2);
p.toJSON();                        // '{"x":1,"y":2,"type":"SmartPoint"}'
new SmartPoint(1, 2).equals(p);    // true

Mixin functions take a class and return an extended one — composable, no diamond problems.

Extending Built-ins

You can subclass Array, Error, Map, etc.:

class Stack extends Array {
  peek() { return this[this.length - 1]; }
}

const s = new Stack();
s.push(1, 2, 3);
s.peek();                          // 3
s instanceof Stack;                // true
s.map(x => x * 2) instanceof Stack;// true — built-in methods return Stack

Two notes:

  • Use super() in the constructor with no args, then push elements (Array is fussy).
  • Subclassing Error is the most common case — see chapter 43.

instanceof Across Inheritance

class A {}
class B extends A {}

const b = new B();
b instanceof B;        // true
b instanceof A;        // true
b instanceof Object;   // true

instanceof walks the prototype chain — B’s prototype eventually links to A.prototype.

A Realistic Example — UI Components

class Component {
  constructor(el) { this.el = el; }
  mount() {
    this.render();
    this.bind();
  }
  unmount() { this.unbind(); }
  render() { /* default no-op */ }
  bind()   { /* default no-op */ }
  unbind() { /* default no-op */ }
}

class CounterButton extends Component {
  count = 0;

  render() {
    this.el.textContent = `Count: ${this.count}`;
  }

  bind() {
    this.onClick = () => { this.count++; this.render(); };
    this.el.addEventListener("click", this.onClick);
  }

  unbind() {
    this.el.removeEventListener("click", this.onClick);
  }
}

new CounterButton(document.querySelector("#btn")).mount();

The base class is the lifecycle skeleton; subclasses fill in the parts that change. This is the right time for extends — a shared, stable template.

Common Mistakes

// ❌ Using `this` before super()
class B extends A {
  constructor() { this.x = 1; super(); }   // ReferenceError
}

// ❌ Forgetting super() in subclass constructor entirely
class B extends A {
  constructor() { /* no super */ }
}
new B();                                   // ReferenceError

// ❌ Overriding without super in a method that depends on parent setup
class B extends A {
  init() { /* forgot super.init() */ }
}

// ❌ Deep inheritance "just because"
class Animal extends LivingThing extends MatterContainer extends Object { … }

// ❌ Trying to call `super` from a static method using wrong syntax
class B extends A { static go() { super.go(); } }   // ✅ super works in static too

Best Practices Checklist

  • ✅ Use extends for genuine “is-a” relationships
  • ✅ Always call super(...) first in a subclass constructor
  • ✅ Use super.method() to extend (not replace) parent behaviour
  • ✅ Keep inheritance shallow — one or two levels max
  • ✅ Reach for composition or mixins when sharing capabilities across unrelated types
  • ✅ Use new.target to make a class non-instantiable
  • ❌ Don’t inherit just to share code — that’s what composition is for
  • ❌ Don’t subclass built-ins (Array, Promise) without a clear reason
  • ❌ Don’t override a method without considering whether super.method() should still run

What’s Next?

You can model whole hierarchies. Next we split a program across files with modules — import, export, default vs named exports, dynamic imports, and how the modern module system replaces the global pollution of script tags.

← Previous: JS Classes Next: JS Modules →

Updated: