JavaScript Class Inheritance: Complete Tutorial

A complete guide to JavaScript class inheritance using extends and super. Learn how to create class hierarchies, call parent constructors, override methods, use super.method(), and understand instanceof with multi-level inheritance.

JavaScriptintermediate
13 min read

Class inheritance allows one class to build on another, reusing and extending behavior. JavaScript's extends keyword establishes the inheritance relationship, and super provides access to the parent class. Understanding how these work โ€” and the rules around them โ€” is core to writing object-oriented JavaScript effectively.

The extends Keyword

extends creates a child class (subclass) that inherits from a parent class (superclass):

javascriptjavascript
class Animal {
  constructor(name) {
    this.name = name;
  }
  speak() {
    return `${this.name} makes a sound.`;
  }
}
 
// Dog extends Animal โ€” inherits constructor and speak()
class Dog extends Animal {
  fetch() {
    return `${this.name} fetches the ball!`;
  }
}
 
const rex = new Dog("Rex");
console.log(rex.speak());  // "Rex makes a sound." (inherited)
console.log(rex.fetch());  // "Rex fetches the ball!" (own method)
console.log(rex instanceof Dog);    // true
console.log(rex instanceof Animal); // true

Dog.prototype is linked to Animal.prototype via the prototype chain. When rex.speak() is called, JavaScript looks on rex (not found), then Dog.prototype (not found), then Animal.prototype (found!).

The super() Call in the Constructor

When a child class defines its own constructor, it MUST call super() before using this. This is enforced by the engine:

javascriptjavascript
class Vehicle {
  constructor(make, model) {
    this.make = make;
    this.model = model;
  }
  describe() {
    return `${this.make} ${this.model}`;
  }
}
 
class Car extends Vehicle {
  constructor(make, model, year) {
    super(make, model); // โ† MUST be first use of 'this'
    this.year = year;   // Own property added after super()
  }
  describe() {
    return `${this.year} ${super.describe()}`; // Call parent describe()
  }
}
 
const myCar = new Car("Toyota", "Camry", 2023);
console.log(myCar.describe()); // "2023 Toyota Camry"

Rule: If you define a constructor in a derived class, super() must be called before referencing this or before the constructor returns. Violating this throws a ReferenceError.

No constructor in child class: If you omit the constructor entirely, JavaScript inserts this automatically:

javascriptjavascript
constructor(...args) {
  super(...args);
}

Overriding Methods

A child class redefines a method from the parent by declaring a method with the same name:

javascriptjavascript
class Shape {
  constructor(color) { this.color = color; }
  area() { return 0; }
  toString() { return `Shape(color=${this.color}, area=${this.area().toFixed(2)})`; }
}
 
class Circle extends Shape {
  constructor(color, radius) {
    super(color);
    this.radius = radius;
  }
  area() { // Overrides Shape.area()
    return Math.PI * this.radius ** 2;
  }
}
 
class Rectangle extends Shape {
  constructor(color, w, h) {
    super(color);
    this.width = w;
    this.height = h;
  }
  area() { // Overrides Shape.area()
    return this.width * this.height;
  }
}
 
const shapes = [
  new Circle("red", 5),
  new Rectangle("blue", 4, 6),
];
 
shapes.forEach(s => console.log(s.toString()));
// "Shape(color=red, area=78.54)"
// "Shape(color=blue, area=24.00)"

Notice toString() is defined once in Shape and calls this.area() โ€” each subclass area() override is picked up automatically because this refers to the actual instance.

super.method() โ€” Calling Parent Methods

Use super.methodName() to call the parent class implementation:

javascriptjavascript
class Logger {
  log(message) {
    console.log(`[LOG] ${message}`);
  }
}
 
class TimestampLogger extends Logger {
  log(message) {
    const ts = new Date().toISOString();
    super.log(`${ts}: ${message}`); // Extends parent behavior
  }
}
 
class PrefixLogger extends TimestampLogger {
  constructor(prefix) {
    super();
    this.prefix = prefix;
  }
  log(message) {
    super.log(`[${this.prefix}] ${message}`); // Chains up through the hierarchy
  }
}
 
const logger = new PrefixLogger("APP");
logger.log("Starting..."); // "[LOG] 2026-...: [APP] Starting..."

For more details on the super keyword's mechanics, see using the super keyword in JavaScript classes.

Multi-Level Inheritance

Inheritance can go multiple levels deep:

javascriptjavascript
class LivingThing {
  constructor(name) { this.name = name; }
  breathe() { return `${this.name} breathes.`; }
}
 
class Animal extends LivingThing {
  constructor(name, legs) {
    super(name);
    this.legs = legs;
  }
  move() { return `${this.name} moves on ${this.legs} legs.`; }
}
 
class Dog extends Animal {
  constructor(name) {
    super(name, 4);
  }
  bark() { return `${this.name} barks!`; }
}
 
const buddy = new Dog("Buddy");
console.log(buddy.breathe()); // "Buddy breathes." (from LivingThing)
console.log(buddy.move());    // "Buddy moves on 4 legs." (from Animal)
console.log(buddy.bark());    // "Buddy barks!" (from Dog)

The prototype chain: buddy โ†’ Dog.prototype โ†’ Animal.prototype โ†’ LivingThing.prototype โ†’ Object.prototype โ†’ null

Class Hierarchy vs Composition

Deep inheritance hierarchies become rigid. JavaScript allows class composition patterns like mixins:

javascriptjavascript
// Mixin โ€” a function that extends a class
const Serializable = (Base) => class extends Base {
  serialize() { return JSON.stringify(this); }
  static deserialize(json) { return Object.assign(new this(), JSON.parse(json)); }
};
 
const Timestamped = (Base) => class extends Base {
  constructor(...args) {
    super(...args);
    this.createdAt = new Date().toISOString();
  }
};
 
class User {
  constructor(name, email) {
    this.name = name;
    this.email = email;
  }
}
 
// Compose behaviors with mixins
class FullUser extends Timestamped(Serializable(User)) {}
 
const u = new FullUser("Alice", "alice@example.com");
console.log(u.serialize()); // JSON string with name, email, createdAt

instanceof in Class Hierarchies

CheckDog extends Animal
dog instanceof Dogtrue
dog instanceof Animaltrue
dog instanceof Objecttrue
animal instanceof Dogfalse (parent is not an instance of child)
Object.getPrototypeOf(Dog.prototype) === Animal.prototypetrue
javascriptjavascript
class Animal {}
class Dog extends Animal {}
class Cat extends Animal {}
 
const d = new Dog();
const c = new Cat();
 
console.log(d instanceof Animal); // true
console.log(c instanceof Dog);    // false
console.log(d instanceof Cat);    // false

Preventing Inheritance With final-Like Patterns

JavaScript has no final keyword, but you can throw in the constructor to prevent direct instantiation:

javascriptjavascript
class AbstractShape {
  constructor() {
    if (new.target === AbstractShape) {
      throw new Error("AbstractShape cannot be instantiated directly");
    }
  }
  area() { throw new Error("area() must be implemented"); }
}
 
class Square extends AbstractShape {
  constructor(side) { super(); this.side = side; }
  area() { return this.side ** 2; }
}
 
// new AbstractShape() โ†’ Error
const s = new Square(5);
console.log(s.area()); // 25

new.target refers to the constructor called with new. In a direct new AbstractShape() call, new.target === AbstractShape.

Rune AI

Rune AI

Key Insights

  • extends sets up prototype chaining: Dog.prototype's [[Prototype]] is Animal.prototype โ€” method lookup traverses the chain automatically
  • super() is mandatory in child constructors: Must be called before any reference to this; the parent constructor allocates and initializes the this object
  • Method overriding is automatic: Define a method with the same name in the child class and it shadows the parent's version for that instance
  • super.method() calls the parent implementation: Useful when you want to extend rather than replace parent behavior
  • instanceof traverses the full chain: An instance of Dog is also an instance of Animal and Object; the check is not just the immediate prototype
RunePowered by Rune AI

Frequently Asked Questions

Why must super() come before this in a derived constructor?

In JavaScript, the child class does not allocate the object itself โ€” the parent constructor does. Until `super()` returns, the `this` object does not exist yet. Accessing `this` before `super()` is like using a variable before it is declared.

Can I extend a constructor function (not a class) with class extends?

Yes. `class Foo extends LegacyConstructorFn {}` works as long as `LegacyConstructorFn` has a compatible `.prototype`.

Can I extend a built-in like Array or Error?

Yes, and this is a legitimate pattern. Extending `Error` for custom error classes is very common. See the article on [creating custom errors in JavaScript](/tutorials/programming-languages/javascript/creating-custom-errors-in-js-complete-tutorial).

What is the difference between override and overwrite?

In JavaScript context: overriding means defining a method in the child class with the same name, so calls to that method on child instances use the child version. Overwriting (monkey patching) directly replaces a property on a prototype object โ€” more explicit and risky.

Conclusion

JavaScript class inheritance with extends and super provides a clean, readable way to build object hierarchies. The child class acquires all parent methods via prototype chaining. The super() call in the constructor is mandatory in derived classes before this is accessible. Method overriding lets child classes specialize behavior while super.method() allows extending it. instanceof traces the full prototype chain, so instances of child classes are also instances of their ancestors.