Inheritance: extends & super

intermediate extends super inheritance method-overriding

Inheritance lets us create a new class based on an existing one. The child class gets all the properties and methods of the parent, and can add its own or override what it inherited. In JavaScript, we use extends to set up this relationship and super to talk to the parent.

Basic inheritance with extends

class Animal {
  constructor(name) {
    this.name = name;
  }

  speak() {
    return `${this.name} makes a sound`;
  }
}

class Dog extends Animal {
  bark() {
    return `${this.name} says Woof!`;
  }
}

const rex = new Dog("Rex");
console.log(rex.speak()); // "Rex makes a sound" — inherited from Animal
console.log(rex.bark());  // "Rex says Woof!" — own method

Dog inherits everything from Animal. We didn’t define a constructor in Dog, so JavaScript automatically calls the parent’s constructor for us.

super() in the constructor

When a child class has its own constructor, we must call super() before using this. This calls the parent’s constructor and sets up the instance properly:

class Animal {
  constructor(name) {
    this.name = name;
  }
}

class Dog extends Animal {
  constructor(name, breed) {
    super(name);        // MUST call super() first!
    this.breed = breed; // now we can use this
  }
}

const rex = new Dog("Rex", "German Shepherd");
console.log(rex.name);  // "Rex"
console.log(rex.breed); // "German Shepherd"

If we forget super() or try to use this before calling it, JavaScript throws a ReferenceError. This is a hard rule — no exceptions.

Method overriding

A child class can define a method with the same name as the parent’s method. This is called method overriding — the child’s version replaces the parent’s for instances of the child class:

class Animal {
  speak() {
    return "Some generic sound";
  }
}

class Cat extends Animal {
  speak() {
    return "Meow!"; // overrides Animal's speak()
  }
}

const cat = new Cat();
console.log(cat.speak()); // "Meow!" — child's version wins

super.method() for parent method calls

Sometimes we don’t want to completely replace the parent’s method — we want to extend it. We can call the parent’s version using super.methodName():

class Animal {
  speak() {
    return "Some sound";
  }

  describe() {
    return `I am a ${this.constructor.name}`;
  }
}

class Dog extends Animal {
  speak() {
    const parentResult = super.speak(); // call parent's speak
    return `${parentResult}... actually, Woof!`;
  }
}

const d = new Dog();
console.log(d.speak()); // "Some sound... actually, Woof!"

Class hierarchy diagram

Here’s how a typical class hierarchy looks with prototype links:

Animal
constructor(name)
speak()
extends
Dog
constructor(name, breed)
bark()
speak() - overridden
Cat
constructor(name, indoor)
purr()
speak() - overridden
extends
GuideDog
constructor(name, breed, handler)
guide()

Inheriting static methods

Static methods are inherited too! The child class can access the parent’s static methods:

class Animal {
  static kingdom() {
    return "Animalia";
  }
}

class Dog extends Animal {}

console.log(Dog.kingdom()); // "Animalia" — inherited static method

This works because Dog.__proto__ === Animal (the class itself, not the prototype). Static methods live on the constructor function, and the child constructor’s prototype points to the parent constructor.

instanceof with inheritance

instanceof walks up the prototype chain, so it returns true for the child class and all its ancestors:

const rex = new Dog("Rex", "Lab");

console.log(rex instanceof Dog);    // true
console.log(rex instanceof Animal); // true
console.log(rex instanceof Object); // true

Multi-level inheritance

We can chain inheritance as deep as we want, but in practice, going more than 2-3 levels deep usually means we should rethink our design (favor composition over deep inheritance):

class Animal {
  constructor(name) { this.name = name; }
  eat() { return `${this.name} eats`; }
}

class Dog extends Animal {
  bark() { return `${this.name} barks`; }
}

class GuideDog extends Dog {
  constructor(name, handler) {
    super(name);
    this.handler = handler;
  }
  guide() { return `${this.name} guides ${this.handler}`; }
}

const buddy = new GuideDog("Buddy", "Alex");
console.log(buddy.eat());   // "Buddy eats" — from Animal
console.log(buddy.bark());  // "Buddy barks" — from Dog
console.log(buddy.guide()); // "Buddy guides Alex" — own method

Common pitfall: forgetting super()

This is the number one mistake beginners make. If our child class has a constructor, we must call super() before doing anything with this:

class Child extends Parent {
  constructor() {
    // this.x = 10; // ReferenceError! Must call super() first
    super();
    this.x = 10;    // now it's fine
  }
}

The reason is that this doesn’t exist in the child constructor until super() creates it. This is by design — it ensures the parent gets to initialize the object first.

In simple language, extends tells JavaScript “this class is based on that class”, and super is how the child talks to the parent. super() in the constructor calls the parent’s constructor, and super.method() calls the parent’s version of a method. Together they give us clean, readable class hierarchies.

References