JavaScriptClass Inheritance (extends, super)

Class Inheritance

extends lets one class build on another. The new class inherits both instance and static members, may add its own, and may override the parent's. The keyword super is the bridge that lets the child reach back into the parent — for the parent constructor, or for a parent method that the child has overridden.

The basic shape

JS
class Animal {
  constructor(name) {
    this.name = name;
  }
  describe() {
    return `I am ${this.name}`;
  }
}

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

  describe() {
    // Reach the parent method via super
    return `${super.describe()}, a ${this.breed}`;
  }
}

const d = new Dog("Rex", "labrador");
console.log(d.describe());          // 'I am Rex, a labrador'
console.log(d instanceof Dog);      // true
console.log(d instanceof Animal);   // true
super in the constructor

In a derived class constructor, this does not exist until you call super(...). Trying to read or write this before that throws.

JS
class Dog extends Animal {
  constructor(name, breed) {
    // this.breed = breed;   // ReferenceError — must super first
    super(name);
    this.breed = breed;       // ok now
  }
}
Don't forget super
Omit `super` entirely in a subclass and the derived constructor still throws when you try to use `this` — or when it ends without calling it. The error message is unambiguous: "Must call super constructor".
super in methods

Inside a method, super.foo() looks up foo on the parent prototype. this is still the actual instance, so any state changes go where you expect.

JS
class Base {
  speak() { return "..."; }
  describe() {
    return `Base says: ${this.speak()}`;
  }
}

class Loud extends Base {
  speak() { return "HELLO"; }
  describe() {
    return super.describe().toUpperCase();
  }
}

console.log(new Loud().describe());   // 'BASE SAYS: HELLO'

Notice that super.describe() calls the parent method, but inside that parent method this is still the Loud instance — so this.speak() resolves to the overridden version. That is dynamic dispatch, and it works across inheritance.

Overriding instance fields

Subclass instance fields are assigned after super() returns. So a parent constructor cannot see the child's field defaults during its own setup.

JS
class Base {
  type = "base";
  constructor() {
    console.log("Base sees:", this.type);
  }
}

class Child extends Base {
  type = "child";
}

new Child();
Base sees: base

The base constructor runs while type is still "base". Only after super() finishes does the child's field declaration overwrite it.

Inheriting static members

Statics inherit too: a subclass can call its parent's static methods through itself, and override them by declaring a static method of the same name.

JS
class Animal {
  static create(name) { return new this(name); }   // 'this' is the actual subclass
  constructor(name) { this.name = name; }
}

class Cat extends Animal {}

const c = Cat.create("Mittens");
console.log(c instanceof Cat);      // true — not just Animal
instanceof and the prototype chain

extends wires up two prototype links — one for instances, one for the class objects themselves.

  • Child.prototype inherits from Parent.prototype — that is what makes inherited instance methods work.

  • The function Child itself inherits from Parent — that is what makes inherited static methods work.

  • a instanceof Parent walks up a's prototype chain looking for Parent.prototype, so subclass instances are also instances of their parents.

JS
class A {}
class B extends A {}

console.log(Object.getPrototypeOf(B.prototype) === A.prototype);   // true
console.log(Object.getPrototypeOf(B) === A);                       // true
console.log(new B() instanceof A);                                  // true
Extending built-ins

You can extend native classes like Error, Array, Map, and Set. The result behaves like the native, with whatever you add on top.

JS
class HttpError extends Error {
  constructor(status, message) {
    super(message);
    this.name = "HttpError";
    this.status = status;
  }
}

try {
  throw new HttpError(404, "not found");
} catch (e) {
  console.log(e instanceof Error, e.status, e.message);
}
true 404 not found
When inheritance is the wrong tool
  • Resist deep hierarchies. Two or three levels is usually plenty.

  • Prefer composition when reuse is about sharing behaviour, not "this is a kind of that".

  • A method that needs to look at the subclass to decide what to do is a sign the abstraction is leaky.

  • For mix-and-match capabilities, see Mixins — multiple inheritance is not directly supported.

The two rules of super
In a derived constructor: call `super(...)` before `this`. In a method: `super.foo()` walks one step up the prototype chain — `this` still refers to the actual instance.
One sentence
`extends` plus `super` wires the prototype chain and lets you reuse, extend, and selectively override what the parent class provides — with full dynamic dispatch.