Skip to content
aviral gupta

// I2.3 · ~34 min · Intermediate

Subclasses with extends and super

After this lesson you build a subclass with extends, call the parent’s constructor with super(), and override a method while reusing the parent’s version.

Lesson 3 of 5 in I2 Objects, prototypes and classes

You will be able to

  • Make a subclass with extends, and call the parent constructor with super(...) before using this
  • Override a method, and reuse the parent’s version with super.method()
  • Follow a subclass’s prototype chain with instanceof and Object.getPrototypeOf
  1. Warm-up · Activity 1 of 7

    Warm-up from lesson I2.2. What does this print?

    class Lamp {
      constructor(color) {
        this.color = color;
      }
      describe() {
        return `a ${this.color} lamp`;
      }
    }
    const lamp = new Lamp("red");
    console.log(lamp.describe(), Object.hasOwn(lamp, "describe"));
  2. Predict · Activity 2 of 7

    Predict before you read on. Cat has no body at all. What does this print?

    class Animal {
      constructor(name) {
        this.name = name;
      }
      speak() {
        return `${this.name} makes a sound`;
      }
    }
    class Cat extends Animal {}
    console.log(new Cat("Mia").speak());
  3. Practice · Activity 3 of 7

    Animal’s constructor stores the name. Fill in the call that runs it from Dog’s constructor.

    class Dog extends Animal {
      constructor(name, breed) {
        ____(name);
        this.breed = breed;
      }
    }
    constructor(name, breed) {(name);
  4. Practice · Activity 4 of 7

    Match each piece of syntax to what it does.

  5. Practice · Activity 5 of 7

    describe() is written in Animal only. What does this print?

    class Animal {
      speak() {
        return "...";
      }
      describe() {
        return `It says ${this.speak()}`;
      }
    }
    class Dog extends Animal {
      speak() {
        return "woof";
      }
    }
    console.log(new Dog().describe());
  6. Brain teaser · Activity 6 of 7

    Brain teaser. B logs before and after super(). In which order are the lines printed?

    class A {
      constructor() {
        console.log("A starts");
      }
    }
    class B extends A {
      constructor() {
        console.log("B starts");
        super();
        console.log("B ends");
      }
    }
    new B();
  7. Apply · Activity 7 of 7

    Mini-task: write a class Vehicle with a constructor(brand, wheels) and describe(), which returns a text such as "Brompton with 2 wheels". Then write ElectricCar extends Vehicle. Its constructor(brand, range) passes 4 wheels to super and stores range; its describe() adds ", range 390 km" to the parent’s text with super.describe(). Show both descriptions and check instanceof both ways.

    Check your work against this list

Build it yourself

Read the worked example, then write the exercises. Your code runs in your browser or on your computer and is never uploaded.

Worked example

Animal, Dog and Puppy

Animal stores a name and has speak() and describe(). Dog extends Animal: its constructor calls super(name) first, then stores the breed, and its speak() overrides Animal’s. Puppy extends Dog without a constructor and builds on Dog’s speak() with super.speak(). describe() is written once, in Animal, yet each animal describes itself with its own speak.

main.js

class Animal {
  constructor(name) {
    this.name = name;
  }
  speak() {
    return `${this.name} makes a sound`;
  }
  describe() {
    return `${this.name}: ${this.speak()}`;
  }
}

class Dog extends Animal {
  constructor(name, breed) {
    super(name); // runs Animal's constructor first
    this.breed = breed;
  }
  speak() {
    return `${this.name} barks`; // overrides Animal's speak
  }
}

class Puppy extends Dog {
  speak() {
    return `${super.speak()} softly`; // builds on Dog's speak
  }
}

const generic = new Animal("Something");
const rex = new Dog("Rex", "terrier");
const bello = new Puppy("Bello", "poodle");

console.log(generic.describe());
console.log(rex.describe());
console.log(bello.describe());
console.log(bello);
console.log(bello instanceof Puppy, bello instanceof Dog, bello instanceof Animal);
console.log(rex instanceof Puppy);
console.log(Object.getPrototypeOf(Dog.prototype) === Animal.prototype);

Run it with

node main.js

Output

Something: Something makes a sound
Rex: Rex barks
Bello: Bello barks softly
Puppy { name: 'Bello', breed: 'poodle' }
true true true
false
true
  • describe() lives in Animal, but this.speak() found the override of each object’s own class.
  • Puppy has no constructor; the default one passed "Bello" and "poodle" on to Dog’s.
  • A puppy is a Dog and an Animal; a plain dog is not a Puppy.
  • Dog.prototype links to Animal.prototype: that is what extends set up.
Change it and run it

Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.

The first run downloads the JavaScript runner (up to 0.1 MB) and keeps it cached. Your code runs in your browser’s own engine and stays on your device.

Exercises

Exercise 1 of 2

A circle is a shape

Shape stores a name, and its describe() uses area(). Circle extends Shape: new Circle(2) should describe itself as "circle with area 13". The starter’s constructor stores the radius but never calls Shape’s constructor, so new Circle(2) throws. Call super("circle") first.

Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.

The first run downloads the JavaScript runner (up to 0.1 MB) and keeps it cached. Your code runs in your browser’s own engine and stays on your device.

Hints
  1. Hint 1

    Run the tests: every one fails with a ReferenceError. What must a subclass constructor do before it uses this?

  2. Hint 2

    super(…) runs Shape’s constructor. Which name does a circle pass to it?

  3. Hint 3

    constructor(radius) { super('circle'); this.radius = radius; }

Show a solution

One way to solve it. Yours can look different and still pass the checks.

export class Shape {
  constructor(name) {
    this.name = name;
  }
  area() {
    return 0;
  }
  describe() {
    return `${this.name} with area ${this.area()}`;
  }
}

export class Circle extends Shape {
  constructor(radius) {
    super('circle');
    this.radius = radius;
  }
  area() {
    return Math.round(Math.PI * this.radius ** 2);
  }
}
Run it on your computer

Install ECMAScript 2026 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.

main.js

export class Shape {
  constructor(name) {
    this.name = name;
  }
  area() {
    return 0;
  }
  describe() {
    return `${this.name} with area ${this.area()}`;
  }
}

export class Circle extends Shape {
  constructor(radius) {
    this.radius = radius;
  }
  area() {
    return Math.round(Math.PI * this.radius ** 2);
  }
}

main.test.js

import {test} from 'node:test';
import assert from 'node:assert/strict';
import {Circle, Shape} from './main.js';

test('new Circle(2) has the name circle and radius 2', () => {
  const c = new Circle(2);
  assert.deepEqual([c.name, c.radius], ['circle', 2], `name and radius are ${c.name} and ${c.radius}`);
});

test('describe uses the circle area', () => {
  const got = new Circle(2).describe();
  assert.equal(got, 'circle with area 13', `describe() gave ${got}`);
});

test('A circle is a Shape', () => {
  assert.ok(new Circle(1) instanceof Shape, 'new Circle(1) should be an instance of Shape');
});

package.json

{
  "type": "module"
}

package.json tells Node.js that the .js files are modules; keep it in the folder.

Run the program:

node main.js

Run the checks (needs learnrun.js in the same folder):

node --test
Download learnrun.js

Exercise 2 of 2

Build on the parent’s describe

A Manager is an Employee with a team. new Manager("Ada", 5000, ["Max", "Mia"]).describe() should return "Ada earns 5000 EUR and leads 2 people". The starter writes its own text and loses the salary part. Reuse Employee’s describe() with super.describe() and add the team part.

Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.

The first run downloads the JavaScript runner (up to 0.1 MB) and keeps it cached. Your code runs in your browser’s own engine and stays on your device.

Hints
  1. Hint 1

    Run the tests: the manager’s text lacks “earns 5000 EUR”. Which method already writes that part?

  2. Hint 2

    super.describe() calls Employee’s describe with this still being the manager.

  3. Hint 3

    return `${super.describe()} and leads ${this.team.length} people`;

Show a solution

One way to solve it. Yours can look different and still pass the checks.

export class Employee {
  constructor(name, salary) {
    this.name = name;
    this.salary = salary;
  }
  describe() {
    return `${this.name} earns ${this.salary} EUR`;
  }
}

export class Manager extends Employee {
  constructor(name, salary, team) {
    super(name, salary);
    this.team = team;
  }
  describe() {
    return `${super.describe()} and leads ${this.team.length} people`;
  }
}
Run it on your computer

Install ECMAScript 2026 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.

main.js

export class Employee {
  constructor(name, salary) {
    this.name = name;
    this.salary = salary;
  }
  describe() {
    return `${this.name} earns ${this.salary} EUR`;
  }
}

export class Manager extends Employee {
  constructor(name, salary, team) {
    super(name, salary);
    this.team = team;
  }
  describe() {
    return `${this.name} leads ${this.team.length} people`;
  }
}

main.test.js

import {test} from 'node:test';
import assert from 'node:assert/strict';
import {Employee, Manager} from './main.js';

test('A manager describes salary and team', () => {
  const got = new Manager('Ada', 5000, ['Max', 'Mia']).describe();
  assert.equal(got, 'Ada earns 5000 EUR and leads 2 people', `describe() gave ${got}`);
});

test('An employee keeps the short description', () => {
  const got = new Employee('Max', 3000).describe();
  assert.equal(got, 'Max earns 3000 EUR', `describe() gave ${got}`);
});

test('A manager is an Employee', () => {
  assert.ok(new Manager('Ada', 5000, []) instanceof Employee, 'a Manager should be an instance of Employee');
});

package.json

{
  "type": "module"
}

package.json tells Node.js that the .js files are modules; keep it in the folder.

Run the program:

node main.js

Run the checks (needs learnrun.js in the same folder):

node --test
Download learnrun.js

Common mistakes

Using this before super()

class Animal {
  constructor(name) {
    this.name = name;
  }
}
class Dog extends Animal {
  constructor(name, breed) {
    this.breed = breed;
    super(name);
  }
}
const rex = new Dog("Rex", "terrier");
console.log(rex.breed);

What Node.js prints

ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor

Why, and the fix

In a subclass, the object that this refers to is set up by the parent’s constructor, so it does not exist until super() has run. Call super(name) as the first thing in the constructor, then set this.breed. The same error comes when a subclass constructor forgets super() altogether.

Calling this.method() inside its own override

class Animal {
  speak() {
    return "...";
  }
}
class Dog extends Animal {
  speak() {
    return `${this.speak()} woof`;
  }
}
console.log(new Dog().speak());

What Node.js prints

RangeError: Maximum call stack size exceeded

Why, and the fix

this.speak() starts the lookup at the dog, so it finds Dog’s speak again: the method calls itself without end until the call stack is full. To reach the parent’s version, write super.speak(), which starts the lookup at Animal.prototype.

Extending an object instead of a class

const animal = {
  speak() {
    return "...";
  }
};
class Dog extends animal {}
console.log(new Dog().speak());

What Node.js prints

TypeError: Class extends value #<Object> is not a constructor or null

Why, and the fix

extends needs a class or a constructor function on its right, something that can be called with new. animal is an ordinary object, as in lesson I2.1. Turn it into class Animal { speak() { … } } and write class Dog extends Animal, or keep objects and use Object.create(animal).

JavaScript in the browser: your browser’s own engine, in a sandboxed worker. Syntax errors are located with acorn 8.18.0, MIT. Licence and source

Exit ticket

5 questions, no hints. Score 80% or more to complete the lesson.

Finish every activity above to unlock the exit ticket.

Report a problem

Spotted something wrong or unclear? Say what, and it will be checked and fixed.

#

At least 20 characters.

Only if you want a reply.

Key ideas

extends and super()

class Dog extends Animal makes Dog a subclass of Animal: a dog gets every Animal method, plus its own. In a subclass, the parent sets up the object, so the constructor must call super(…) with the parent’s arguments before it touches this. Code before super() may run, as long as it does not use this. Use this first, or forget super() completely, and new throws a ReferenceError. A subclass without a constructor gets a default one that passes all its arguments on to super.

Overriding and super.method()

A method in the subclass with the same name as a parent method overrides it: the lookup finds the subclass’s version first. This also counts when a parent method calls this.speak(); for a dog, this is the dog, so the dog’s speak runs. To build on the parent’s method instead of replacing it, call super.speak() inside the override. Writing this.speak() there instead calls the override itself, again and again, until a RangeError stops it.

The chain of a subclass

extends adds a link: Dog.prototype’s prototype is Animal.prototype, whose prototype is Object.prototype. That is how a dog finds Animal’s methods. instanceof checks the whole chain, so a Puppy is also a Dog and an Animal, but a plain Dog is not a Puppy. extends needs a class or constructor function on its right; an ordinary object throws a TypeError. Built-in classes such as Array or Map can be extended too. In module I4 you extend Error to make your own error types.

Sources

Last reviewed October 5, 2026