AI InterviewSearch…Search problems, topics, or pages…Ctrl KGet started
Skip to content

05 Prototypes and classes / Lesson 4 of 5

Inheritance — extends, super, overriding and extending built-ins

13 min · +10 XP · 2 exercises · 5 questions

Read the text13 min
Exercises0/2
Quiz5 q

Quick answer

How do extends and super work in JavaScript?

In JavaScript, class Sub extends Base sets up two prototype links: Sub.prototype inherits from Base.prototype, so instances find parent methods, and Sub inherits from Base, so static members are found too. super() calls the parent constructor, which creates this, so it must run before this is used; super.method() calls the parent's version of a method.

extends links two prototype chains: the subclass's prototype inherits from the parent's prototype (so instances find parent methods), and the subclass constructor inherits from the parent constructor (so statics are found too). super is the keyword for reaching the parent from either place. The rules are few but strict — call super() before touching this, fields initialise after it, super.method() uses the home object not the receiver — and the lesson closes with the two things people actually do with inheritance in JavaScript: extend Error, Map and Array, and prefer composition for everything else.

The two chains extends creates

class Shape {
  constructor(name) { this.name = name; }
  area() { return 0; }
  describe() { return `${this.name} with area ${this.area().toFixed(2)}`; }
  static create(...args) { return new this(...args); }
}
class Circle extends Shape {
  constructor(r) {
    super("circle");        // must run before any use of this
    this.r = r;
  }
  area() { return Math.PI * this.r ** 2; }                        // override
  describe() { return super.describe() + ` (r=${this.r})`; }      // extend the parent's version
}

const c = new Circle(1);
Object.getPrototypeOf(Circle.prototype) === Shape.prototype;   // instance chain: c → Circle.prototype → Shape.prototype → Object.prototype
Object.getPrototypeOf(Circle) === Shape;                       // static chain: Circle → Shape → Function.prototype
Circle.create(2) instanceof Circle;                            // true — static this is Circle
c instanceof Shape;                                            // true

Overriding is just shadowing on the nearer prototype: c.area finds Circle.prototype.area first. describe, found on Shape.prototype, calls this.area() — and this is c, so it dispatches to the override. That late binding is the entire point of polymorphism, and it needs no annotations.

super() and construction order

In a derived class the this object is created by the base constructor — super() is the call that creates it — so:

  • A derived constructor must call super() before reading or writing this (ReferenceError: Must call super constructor in derived class before accessing 'this').
  • A derived class with no constructor gets constructor(...args) { super(...args); } for free.
  • The derived class's fields initialise right after super() returns, before the rest of the constructor body. So if a base constructor calls an overridden method, that method runs before the subclass's fields exist:
class Base { constructor() { this.init(); } init() {} }
class Derived extends Base {
  value = 42;
  init() { console.log(this.value); }     // logs undefined — field not yet initialised
}
new Derived();

The fix is a design rule, not a trick: base constructors do not call overridable methods.

super.method() and the home object

super.describe() looks up describe on the prototype of the class where the method was written (the home object), not on this's prototype. That is what makes two levels of overriding work — each super call climbs exactly one level from where it was written, regardless of how deep the receiver's class is. It also means super in a method copied to another object still refers to the original parent. Static methods can use super.staticMethod() too, climbing the static chain.

Extending built-ins

class HttpError extends Error {
  constructor(status, message) {
    super(message);                       // sets message and captures the stack
    this.name = "HttpError";              // otherwise the stack says "Error"
    this.status = status;
  }
}
try { throw new HttpError(404, "no such page"); }
catch (e) { e instanceof HttpError && e instanceof Error; }   // true

class DefaultMap extends Map {
  constructor(makeDefault) { super(); this.makeDefault = makeDefault; }
  get(key) { if (!this.has(key)) this.set(key, this.makeDefault()); return super.get(key); }
}

class Stack extends Array {
  peek() { return this[this.length - 1]; }
}
const s = Stack.from([1, 2, 3]);
s.map((x) => x * 2) instanceof Stack;     // true — array methods build the subclass via Symbol.species
Array.isArray(s);                         // true

Subclassing Error is the one inheritance every codebase needs (module 7 goes deep). Map/Set subclasses work cleanly. Array subclasses work but carry the Symbol.species machinery (methods return the subclass) and were impossible before 2015 — prefer a class that holds an array unless you truly want array behaviour. Promise subclasses are legal and almost never worth it.

Mixins: multiple inheritance by function

JavaScript has single inheritance; a mixin is a function from class to class:

const Serializable = (Base) => class extends Base {
  toJSON() { return { ...this, kind: this.constructor.name }; }
};
const Comparable = (Base) => class extends Base {
  compareTo(other) { return this.valueOf() - other.valueOf(); }
};
class Money extends Serializable(Comparable(Object)) { /* … */ }

Each application inserts one prototype into the chain. Use sparingly — three mixins deep and nobody knows where a method comes from.

Composition over inheritance

A class hierarchy is right when subclasses are genuinely kinds of the parent, share most behaviour, and the hierarchy is shallow (one or two levels). It is wrong when you reach for it to share a utility method, when the "is-a" is a stretch (Button extends Rectangle), or when the tree keeps growing sideways. The alternative is holding an instance of the thing and delegating (this.logger.log(...)), or passing behaviour in as functions. Most production JavaScript has few deep hierarchies and many small classes that contain other objects.

Common mistakes

  • Using this before super(); forgetting super(...args) when you add a constructor.
  • Base constructors calling methods the subclass overrides (fields are not ready).
  • Forgetting this.name = "…" in an Error subclass, so logs say Error.
  • Overriding a method without calling super.method() when the parent's version did required work.
  • Deep hierarchies for code reuse; extends Array when an array field would do.

Interview angle

  • "What does extends set up?" Two prototype links: Sub.prototype → Base.prototype for instances and Sub → Base for statics.
  • "Why must super() be called first?" The base constructor creates this; before it there is no object.
  • "How does method overriding dispatch?" Property lookup finds the nearest prototype's version; this is the receiver, so parent code calls child overrides.
  • "How do you make a custom error?" class X extends Error, super(message), set name, add fields.
  • "Inheritance versus composition?" Extend for real is-a relationships, shallow; otherwise hold and delegate.

Key takeaways

  • extends links instance and static chains; instanceof follows the instance chain.
  • super() first in derived constructors; fields initialise after it; base constructors should not call overridables.
  • super.method() climbs one level from where the method was written (home object).
  • Extend Error (set name), Map/Set freely; Array works via Symbol.species but usually hold an array instead.
  • Mixins are functions from class to class; prefer composition for sharing behaviour.

Common questions

Why must super() be called before this in a derived class?

In a derived class the base constructor creates the this object, and super() is the call that runs it. Before super() returns there is no object, so touching this throws ReferenceError: Must call super constructor in derived class before accessing 'this'.

When are subclass fields initialised in JavaScript?

Right after super() returns, before the rest of the derived constructor body. So if a base constructor calls a method the subclass overrides, that method runs before the subclass's fields exist and reads undefined — base constructors should not call overridable methods.

How do you create a custom error class in JavaScript?

Write class HttpError extends Error, call super(message) to set the message and capture the stack, set this.name = "HttpError" so logs name it correctly, and add fields such as status. Instances then pass both instanceof HttpError and instanceof Error.

Does JavaScript support multiple inheritance?

No, a class has exactly one parent. Mixins give a similar effect: a mixin is a function that takes a class and returns class extends Base { … }, and each application inserts one more prototype into the chain. Use them sparingly, because deep stacks hide where a method comes from.

Should I use inheritance or composition in JavaScript?

Inherit only for genuine is-a relationships that share most behaviour and stay shallow, one or two levels. To share a utility or model a loose relationship, compose instead: hold an instance of the other object and delegate to it, or pass behaviour in as functions.

Mark as readSign in to keep your place.

JavaScript Class Inheritance: extends, super and Overriding

JavaScript extends links two prototype chains, for instances and for statics. Why super() must come first, how overriding dispatches, and custom errors.

  • Course: JavaScript study plan
  • Module: Prototypes and classes
  • Kind: Lesson
  • Reading time: 13 min
  • Runtime: Node 16

How do extends and super work in JavaScript?

In JavaScript, class Sub extends Base sets up two prototype links: Sub.prototype inherits from Base.prototype, so instances find parent methods, and Sub inherits from Base, so static members are found too. super() calls the parent constructor, which creates this, so it must run before this is used; super.method() calls the parent's version of a method.

Lesson

extends links two prototype chains: the subclass's prototype inherits from the parent's prototype (so instances find parent methods), and the subclass constructor inherits from the parent constructor (so statics are found too). super is the keyword for reaching the parent from either place. The rules are few but strict — call super() before touching this, fields initialise after it, super.method() uses the home object not the receiver — and the lesson closes with the two things people actually do with inheritance in JavaScript: extend Error, Map and Array, and prefer composition for everything else.

The two chains extends creates

class Shape {
  constructor(name) { this.name = name; }
  area() { return 0; }
  describe() { return `${this.name} with area ${this.area().toFixed(2)}`; }
  static create(...args) { return new this(...args); }
}
class Circle extends Shape {
  constructor(r) {
    super("circle");        // must run before any use of this
    this.r = r;
  }
  area() { return Math.PI * this.r ** 2; }                        // override
  describe() { return super.describe() + ` (r=${this.r})`; }      // extend the parent's version
}

const c = new Circle(1);
Object.getPrototypeOf(Circle.prototype) === Shape.prototype;   // instance chain: c → Circle.prototype → Shape.prototype → Object.prototype
Object.getPrototypeOf(Circle) === Shape;                       // static chain: Circle → Shape → Function.prototype
Circle.create(2) instanceof Circle;                            // true — static this is Circle
c instanceof Shape;                                            // true

Overriding is just shadowing on the nearer prototype: c.area finds Circle.prototype.area first. describe, found on Shape.prototype, calls this.area() — and this is c, so it dispatches to the override. That late binding is the entire point of polymorphism, and it needs no annotations.

super() and construction order

In a derived class the this object is created by the base constructor — super() is the call that creates it — so:

  • A derived constructor must call super() before reading or writing this (ReferenceError: Must call super constructor in derived class before accessing 'this').
  • A derived class with no constructor gets constructor(...args) { super(...args); } for free.
  • The derived class's fields initialise right after super() returns, before the rest of the constructor body. So if a base constructor calls an overridden method, that method runs before the subclass's fields exist:
class Base { constructor() { this.init(); } init() {} }
class Derived extends Base {
  value = 42;
  init() { console.log(this.value); }     // logs undefined — field not yet initialised
}
new Derived();

The fix is a design rule, not a trick: base constructors do not call overridable methods.

super.method() and the home object

super.describe() looks up describe on the prototype of the class where the method was written (the home object), not on this's prototype. That is what makes two levels of overriding work — each super call climbs exactly one level from where it was written, regardless of how deep the receiver's class is. It also means super in a method copied to another object still refers to the original parent. Static methods can use super.staticMethod() too, climbing the static chain.

Extending built-ins

class HttpError extends Error {
  constructor(status, message) {
    super(message);                       // sets message and captures the stack
    this.name = "HttpError";              // otherwise the stack says "Error"
    this.status = status;
  }
}
try { throw new HttpError(404, "no such page"); }
catch (e) { e instanceof HttpError && e instanceof Error; }   // true

class DefaultMap extends Map {
  constructor(makeDefault) { super(); this.makeDefault = makeDefault; }
  get(key) { if (!this.has(key)) this.set(key, this.makeDefault()); return super.get(key); }
}

class Stack extends Array {
  peek() { return this[this.length - 1]; }
}
const s = Stack.from([1, 2, 3]);
s.map((x) => x * 2) instanceof Stack;     // true — array methods build the subclass via Symbol.species
Array.isArray(s);                         // true

Subclassing Error is the one inheritance every codebase needs (module 7 goes deep). Map/Set subclasses work cleanly. Array subclasses work but carry the Symbol.species machinery (methods return the subclass) and were impossible before 2015 — prefer a class that holds an array unless you truly want array behaviour. Promise subclasses are legal and almost never worth it.

Mixins: multiple inheritance by function

JavaScript has single inheritance; a mixin is a function from class to class:

const Serializable = (Base) => class extends Base {
  toJSON() { return { ...this, kind: this.constructor.name }; }
};
const Comparable = (Base) => class extends Base {
  compareTo(other) { return this.valueOf() - other.valueOf(); }
};
class Money extends Serializable(Comparable(Object)) { /* … */ }

Each application inserts one prototype into the chain. Use sparingly — three mixins deep and nobody knows where a method comes from.

Composition over inheritance

A class hierarchy is right when subclasses are genuinely kinds of the parent, share most behaviour, and the hierarchy is shallow (one or two levels). It is wrong when you reach for it to share a utility method, when the "is-a" is a stretch (Button extends Rectangle), or when the tree keeps growing sideways. The alternative is holding an instance of the thing and delegating (this.logger.log(...)), or passing behaviour in as functions. Most production JavaScript has few deep hierarchies and many small classes that contain other objects.

Common mistakes

  • Using this before super(); forgetting super(...args) when you add a constructor.
  • Base constructors calling methods the subclass overrides (fields are not ready).
  • Forgetting this.name = "…" in an Error subclass, so logs say Error.
  • Overriding a method without calling super.method() when the parent's version did required work.
  • Deep hierarchies for code reuse; extends Array when an array field would do.

Interview angle

  • "What does extends set up?" Two prototype links: Sub.prototype → Base.prototype for instances and Sub → Base for statics.
  • "Why must super() be called first?" The base constructor creates this; before it there is no object.
  • "How does method overriding dispatch?" Property lookup finds the nearest prototype's version; this is the receiver, so parent code calls child overrides.
  • "How do you make a custom error?" class X extends Error, super(message), set name, add fields.
  • "Inheritance versus composition?" Extend for real is-a relationships, shallow; otherwise hold and delegate.

Key takeaways

  • extends links instance and static chains; instanceof follows the instance chain.
  • super() first in derived constructors; fields initialise after it; base constructors should not call overridables.
  • super.method() climbs one level from where the method was written (home object).
  • Extend Error (set name), Map/Set freely; Array works via Symbol.species but usually hold an array instead.
  • Mixins are functions from class to class; prefer composition for sharing behaviour.

Common questions

Why must super() be called before this in a derived class?

In a derived class the base constructor creates the this object, and super() is the call that runs it. Before super() returns there is no object, so touching this throws ReferenceError: Must call super constructor in derived class before accessing 'this'.

When are subclass fields initialised in JavaScript?

Right after super() returns, before the rest of the derived constructor body. So if a base constructor calls a method the subclass overrides, that method runs before the subclass's fields exist and reads undefined — base constructors should not call overridable methods.

How do you create a custom error class in JavaScript?

Write class HttpError extends Error, call super(message) to set the message and capture the stack, set this.name = "HttpError" so logs name it correctly, and add fields such as status. Instances then pass both instanceof HttpError and instanceof Error.

Does JavaScript support multiple inheritance?

No, a class has exactly one parent. Mixins give a similar effect: a mixin is a function that takes a class and returns class extends Base { … }, and each application inserts one more prototype into the chain. Use them sparingly, because deep stacks hide where a method comes from.

Should I use inheritance or composition in JavaScript?

Inherit only for genuine is-a relationships that share most behaviour and stay shallow, one or two levels. To share a utility or model a loose relationship, compose instead: hold an instance of the other object and delegate to it, or pass behaviour in as functions.

Exercises

A shape hierarchy

Given Shape (with describe() printing <name>: area=<2dp> perimeter=<2dp>), write Circle(r), Rect(w, h) and Square(s) extends Rect. Circle.describe appends r=<r>, Rect.describe appends <w>x<h> — both by calling super.describe(). A square is a rect named square built with super(s, s, "square"). Read one shape per line (circle r, rect w h, square s), print each describe(), then total area=<2dp> shapes=<n> rects=<count instanceof Rect> squares=<count instanceof Square>, then chain=<constructor names along a Square instance's prototype chain>.

Example: circle 1, rect 2 3, square 4 →

circle: area=3.14 perimeter=6.28 r=1
rect: area=6.00 perimeter=10.00 2x3
square: area=16.00 perimeter=16.00 4x4
total area=25.14 shapes=3 rects=2 squares=1
chain=Square -> Rect -> Shape -> Object

Extending Map, Array and Error

Write three built-in subclasses. DefaultMap extends Map takes a makeDefault function and its get creates the default for a missing key. Stack extends Array adds peek(). HttpError extends Error takes (status, message), sets name to "HttpError" and stores status. Input: line 1 words, line 2 numbers, line 3 a status code and a message. Print counts: <word=count sorted by word> instanceofMap=<> using the DefaultMap, then stack: peek=<> length=<> isArray=<Array.isArray> mapKeepsClass=<stack.map(...) instanceof Stack> doubledPeek=<peek of the doubled stack>, then throw the error and print <name> <status>: <message> instanceofError=<> stackStartsWithName=<e.stack.startsWith("HttpError")>.

Example: to be or not to be / 3 1 4 1 5 / 404 page not found →

counts: be=2 not=1 or=1 to=2 instanceofMap=true
stack: peek=5 length=5 isArray=true mapKeepsClass=true doubledPeek=10
HttpError 404: page not found instanceofError=true stackStartsWithName=true

In this module: Prototypes and classes

← Class syntax — fields, methods, statics, private and accessors · Property descriptors and the object protocols — toString, valueOf, toJSON, Symbol.toPrimitive →