AlgoInsight

Free lesson · JavaScript Basic to Advance

Inheritance with extends and super

Easy5 min read

Building Classes That Build on Other Classes

Inheritance lets you create a class that builds on another class. The new class — usually called the child or subclass — inherits everything from the parent, then adds, modifies, or replaces what it needs.

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

  describe() {
    return `${this.name} is an animal`;
  }
}

class Dog extends Animal {
  constructor(name, breed) {
    super(name);   // call the parent constructor
    this.breed = breed;
  }

  bark() {
    return `${this.name} says woof!`;
  }
}

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

console.log(rex.name);        // "Rex" — set in Animal's constructor
console.log(rex.breed);       // "Labrador" — set in Dog's constructor
console.log(rex.describe());  // "Rex is an animal" — inherited method
console.log(rex.bark());      // "Rex says woof!" — Dog's own method

Dog extends Animal says "a Dog is an Animal, plus more." Every instance of Dog has access to everything in Animal — properties, methods, the constructor logic — and can add its own.

This Topic covers extends, super, method overriding, and the underlying prototype chain that makes class inheritance work. Once again: this is all sugar over what Nikhil covered in Module 7. The class syntax just makes the chain easier to read.

Interactive animation: Dog adds fetch, overrides speak and inherits eat from Animal - one object answering from two classes.

What extends Actually Does

When you write class Dog extends Animal, JavaScript wires up two prototype chains:

class Animal { describe() { return "an animal"; } }
class Dog extends Animal { bark() { return "woof!"; } }

const rex = new Dog();

// The chains
console.log(Object.getPrototypeOf(rex) === Dog.prototype);              // true
console.log(Object.getPrototypeOf(Dog.prototype) === Animal.prototype); // true
console.log(Object.getPrototypeOf(Animal.prototype) === Object.prototype); // true

The instance chain: rex → Dog.prototype → Animal.prototype → Object.prototype → null.

When you call rex.describe(), JavaScript searches the chain: not on rex itself, not on Dog.prototype, but found on Animal.prototype. The method runs with this bound to rex.

extends also wires up a second chain between the constructor functions themselves, which matters for static methods (covered in Topic 8.5):

class Animal {}
class Dog extends Animal {}

// The constructor chain (less commonly discussed)
console.log(Object.getPrototypeOf(Dog) === Animal);   // true

For most purposes, just remember the instance chain. The instance can access anything on Dog.prototype, or Animal.prototype, or any further-up prototype.

graph TD
    A[rex instance] --> B[Dog.prototype<br/>bark]
    B --> C[Animal.prototype<br/>describe]
    C --> D[Object.prototype<br/>toString, hasOwnProperty]
    D --> E[null]

    style A fill:#10b981,color:#fff
    style B fill:#7c3aed,color:#fff
    style C fill:#7c3aed,color:#fff
This diagram shows the prototype chain created by class inheritance. An instance of Dog has a chain that goes through Dog's prototype, then Animal's prototype, then Object's prototype, and finally null. When you access a property on the instance, JavaScript walks this chain looking for the property. Methods defined on any ancestor in the chain are accessible to the instance.

The super Keyword

super does two distinct things depending on where it's used.

Inside a constructor: super(...) calls the parent's constructor.

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

class Dog extends Animal {
  constructor(name, breed) {
    super(name);        // run Animal's constructor with name
    this.breed = breed;
  }
}

const rex = new Dog("Rex", "Labrador");
console.log(rex.name);    // "Rex" — set by Animal's constructor
console.log(rex.alive);   // true — set by Animal's constructor
console.log(rex.breed);   // "Labrador" — set by Dog's constructor

When new Dog(...) runs, JavaScript needs to:

  1. Run Animal's constructor to set up name and alive
  2. Run Dog's constructor to set up breed

super(name) is how Dog's constructor invokes Animal's. It must be called before accessing this in the subclass constructor. If you reference this before calling super, JavaScript throws an error:

class BrokenDog extends Animal {
  constructor(name, breed) {
    // console.log(this.name);   // ❌ ReferenceError: this is not initialized
    super(name);
    this.breed = breed;
  }
}

The rule: in a subclass constructor, super(...) first, this.something = ... after. The JavaScript engine enforces this because before super runs, the parent's setup hasn't happened, so this isn't fully ready.

Inside a method: super.method() calls the parent's version of the method.

class Animal {
  describe() {
    return `${this.name} is an animal`;
  }
}

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

  describe() {
    const parentDescription = super.describe();   // call Animal's describe
    return `${parentDescription}, specifically a dog`;
  }
}

const rex = new Dog("Rex");
console.log(rex.describe());   // "Rex is an animal, specifically a dog"

super.describe() inside Dog's describe method calls the parent class's version of describe. This is useful when a subclass wants to extend the parent's behavior — do everything the parent did, plus more.

Interactive animation: Manager's describe calls super.describe() for the parent's answer, then adds its own team to it.

Method Overriding

When a child class defines a method with the same name as a parent class method, the child's version overrides the parent's. Instances of the child class use the child's version.

class Animal {
  greet() {
    return "I am an animal";
  }
}

class Dog extends Animal {
  greet() {
    return "I am a dog";
  }
}

class Puppy extends Dog {
  greet() {
    const dogGreet = super.greet();   // calls Dog.greet, not Animal.greet
    return `${dogGreet}, and a young one`;
  }
}

const rex = new Animal();
const max = new Dog();
const buddy = new Puppy();

console.log(rex.greet());     // "I am an animal"
console.log(max.greet());     // "I am a dog" — overrides Animal.greet
console.log(buddy.greet());   // "I am a dog, and a young one"

Overriding works through the same prototype chain lookup. When you call max.greet(), JavaScript searches max → Dog.prototype → Animal.prototype. It finds greet on Dog.prototype first and uses that — never reaching Animal's version. This is what makes overriding work: the lookup finds the closer ancestor first.

super.greet() inside Puppy.greet skips past Puppy's version (where we currently are) and walks up to the next definition, which is on Dog.prototype. It does not go to Animal — super refers to the immediate parent class.

Interactive animation: Two speak methods on rex's chain - the lookup finds Dog.prototype's first, so overriding is just chain ordering.

When to Use Inheritance

Inheritance is powerful, but it's also frequently overused. The rule I share with engineers I mentor: use inheritance when there's a real "is-a" relationship. A Dog is an Animal. A SavingsAccount is an Account. An AdminUser is a User.

When the relationship is "has-a" rather than "is-a", composition is usually a better choice. A Car has an Engine — it's not "an engine that also drives." A Profile has Address information — it's not "an address that also identifies a user." We cover composition vs. inheritance in detail in Topic 8.6.

Three signs you might be overusing inheritance:

1. Deep hierarchies. When your inheritance tree is more than 3-4 levels deep, you're usually introducing more complexity than you're removing. Each level adds coupling between layers.

2. Repeated overriding. If a method is overridden at every level, what's actually being inherited? The structure exists, but the parent's logic is constantly being replaced. That's a smell that suggests the abstraction is wrong.

3. Forcing things to fit. If you find yourself stretching the definition of "is-a" to justify inheritance — "well, a SearchResult is kind of like a User..." — you're inheriting for convenience, not for the right reasons. Use composition or interfaces (which JavaScript doesn't have natively, but you can approximate with duck typing).

Three decades of OO experience boils down to: prefer composition. Use inheritance for clear is-a relationships, sparingly.

Common Pitfalls

Warning: The inheritance traps.

Forgetting to call super() in a subclass constructor. This throws a runtime error. If your subclass has a constructor, super(...) must come before any use of this.

Calling super.method() outside of a method that overrides it. Won't necessarily error, but you should generally only use super.method() inside an overriding method that's calling the parent version of itself.

Deep inheritance hierarchies. A class extending a class extending a class extending a class is rarely clearer than composition. If you find yourself needing four levels, step back and reconsider the design.

Confusing the constructor chain with the prototype chain. extends wires up both. The prototype chain handles instance methods. The constructor chain handles static methods. They're related but separate (covered in Topic 8.5).

Interactive animation: Touching this before super() in a child constructor throws a ReferenceError - the parent has to run first.

Why This Matters in Interviews

Interview: "Explain inheritance in JavaScript classes" expects an answer covering extends, super (both forms), method overriding, and ideally a mention that the underlying mechanism is the prototype chain.

"When should you use inheritance vs composition?" is a senior question. The answer: inheritance for genuine is-a relationships, composition for has-a or behavioral reuse. We cover this deeply in Topic 8.6.

Topic 8.7 — Interview Lens: this, Classes, and OO Patterns — covers the deeper interview treatment of inheritance questions.

Try It

Exercise: Build a small class hierarchy:

  1. A Vehicle class with wheels (default 4), speed (default 0), and methods accelerate(amount), brake(amount), and describe()
  2. A Car class extending Vehicle with an additional passengers field and an overriding describe() that includes passenger count (and uses super.describe())
  3. A SportsCar class extending Car with a topSpeed field, a new method boost() that increases speed by 50, and an overriding describe() that adds top speed info

Then verify with instanceof:

const sc = new SportsCar(/* ... */);

console.log(sc instanceof SportsCar);   // true
console.log(sc instanceof Car);         // true
console.log(sc instanceof Vehicle);     // true
console.log(sc instanceof Object);      // true

All four should be true because Object.prototype is at the top of every chain. By the time this works, you've built a real class hierarchy and exercised extends, super, and method overriding.

Back to JavaScript Basic to Advance