JavaScript Inheritance
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:
- A subclass constructor must call
super(...)before usingthis. superin 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 (Arrayis fussy). - Subclassing
Erroris 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
extendsfor 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.targetto 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.