Inheritance with extends and super

A class that extends another starts with everything the parent has, calls super to let the parent set up its part, and can add methods or override the parent's with its own version.

Journeyman · JavaScript · OOP · Updated

After this lesson you can

  • Write a class that extends another and calls super in its constructor
  • Override a method and call the parent's version with super

Key takeaways

  • class Fighter extends Ship makes Fighter a child of Ship. Every fighter gets Ship's properties and methods without writing them again.
  • A child's constructor must call super(...) before it uses this. super runs the parent's constructor.
  • A child can add its own methods, such as fire(), that only its instances have.
  • A child overrides a method by writing one with the same name. Inside it, super.describe() still runs the parent's version.
  • A loop can call describe() on fighters, freighters and plain ships alike, and each answers in its own way.

Red Squadron needs more than one kind of ship. A fighter is a ship with missiles. A freighter is a ship with a cargo hold. Both still have a name, shields and fuel, and both still take hits. Copying the whole Ship class twice would mean fixing every bug three times. Real programs hit this all the time: a store's products where books, shirts and gift cards share most things, or a game's enemies, each with its own trick. Inheritance lets one class build on another, and it's how the libraries you'll use later (and this course's Virtual Pet project, with its Dragon that extends Pet) are put together.

Every example in this lesson starts with this Ship class. Make a file named hangar.js and put the class at the top. To run an example, paste it under the class, in place of the example before it:

class Ship {
  constructor(name, shields, fuel) {
    this.name = name;
    this.shields = shields;
    this.fuel = fuel;
  }

  describe() {
    return `${this.name}: shields ${this.shields}, fuel ${this.fuel}`;
  }
}

A ship, and then some

Think of a basic cheese pizza and the specials built on it. A pepperoni pizza is a cheese pizza with pepperoni on top: same dough, same sauce, same oven, plus one thing. Nobody writes the dough recipe again for it.

In JavaScript, extends says "this class is that class, plus some". The class it builds on is the parent class, and the new one is the child class. A child with nothing in it at all already has everything the parent has:

class Freighter extends Ship {}

const muleOne = new Freighter("Mule One", 150, 80);
console.log(muleOne.describe());
Mule One: shields 150, fuel 80

Freighter has no constructor and no describe of its own, yet Mule One gets both from Ship. A good test for inheritance is the words "is a": a freighter is a ship, so extends fits.

super: let the parent set up its part

A freighter also carries cargo, so its constructor needs a fourth value. The child's constructor calls super(...) first: super runs the parent's constructor, which stores the name, shields and fuel. Then the child stores what's new:

class Freighter extends Ship {
  constructor(name, shields, fuel, cargo) {
    super(name, shields, fuel);
    this.cargo = cargo;
  }
}

const muleOne = new Freighter("Mule One", 150, 80, 40);
console.log(muleOne.cargo);
console.log(muleOne.describe());
40
Mule One: shields 150, fuel 80

super(...) must come before any use of this in a child's constructor. Swap the two lines, and the program stops when it builds Mule One:

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

The error's derived class is another name for a child class. Until the parent has built its part of the object, there's no this to store the cargo on.

Adding methods of its own

A child can have methods the parent doesn't. Only fighters fire missiles:

class Fighter extends Ship {
  constructor(name, shields, fuel, missiles) {
    super(name, shields, fuel);
    this.missiles = missiles;
  }

  fire() {
    if (this.missiles === 0) {
      return `${this.name} is out of missiles!`;
    }
    this.missiles = this.missiles - 1;
    return `${this.name} fires! ${this.missiles} left`;
  }
}

const comet = new Fighter("Comet", 100, 50, 2);
console.log(comet.fire());
console.log(comet.fire());
console.log(comet.fire());
Comet fires! 1 left
Comet fires! 0 left
Comet is out of missiles!

fire reads this.name, which Ship's constructor stored, and this.missiles, which Fighter's stored: it's all one object. A plain Ship has no fire: inheritance goes from parent to child, never the other way.

Try it. In hangar.js, give Freighter a method unload(crates) that takes crates out of the cargo and returns a line such as Mule One unloads 15 crates, 25 left. Build Mule One with 40 crates and unload twice.

Overriding: your own version of a parent's method

Comet's describe() still says nothing about missiles, because it's Ship's version. A child overrides a method by writing one with the same name: JavaScript uses the child's version for the child's instances.

To reuse the parent's line instead of writing it out again, call super.describe(): inside a child's method, super. reaches the parent's version:

class Fighter extends Ship {
  constructor(name, shields, fuel, missiles) {
    super(name, shields, fuel);
    this.missiles = missiles;
  }

  describe() {
    return `${super.describe()}, missiles ${this.missiles}`;
  }
}

const comet = new Fighter("Comet", 100, 50, 4);
console.log(comet.describe());
Comet: shields 100, fuel 50, missiles 4

If Ship's line ever changes (say it adds a ship's speed), every fighter's line follows, because it's built from super.describe().

One thing a child can't reach: its parent's private fields. If Ship kept #shields private, Fighter's methods would read them through Ship's getter, this.shields, like any other outside code.

One loop, many kinds of ship

Here's where it pays off. A fleet holds fighters, freighters and plain ships in one array, and a loop asks each to describe itself:

class Fighter extends Ship {
  constructor(name, shields, fuel, missiles) {
    super(name, shields, fuel);
    this.missiles = missiles;
  }

  describe() {
    return `${super.describe()}, missiles ${this.missiles}`;
  }
}

class Freighter extends Ship {
  constructor(name, shields, fuel, cargo) {
    super(name, shields, fuel);
    this.cargo = cargo;
  }

  describe() {
    return `${super.describe()}, cargo ${this.cargo} crates`;
  }
}

const fleet = [
  new Fighter("Comet", 100, 50, 4),
  new Freighter("Mule One", 150, 80, 40),
  new Ship("Scout", 60, 30),
];

for (const ship of fleet) {
  console.log(ship.describe());
}
Comet: shields 100, fuel 50, missiles 4
Mule One: shields 150, fuel 80, cargo 40 crates
Scout: shields 60, fuel 30

The loop doesn't check what kind of ship it has. It calls describe(), and each ship answers in its own way. In your textbook this is called polymorphism ("many shapes"); parent and child classes are also called base and derived classes, or superclass and subclass.

A family tree of ships: Ship at the top with a name, shields, fuel and describe; Fighter and Freighter below it, each adding one thing of its own, missiles or cargo

Check yourself

2 questions to try on the lesson page.

So what

extends and super let you write what's shared once and only what's different in each child. When a game adds a new kind of enemy, or a store a new kind of product, it's one small class, and every loop that already works with the parent works with it too.

Key terms

Cheat sheets

Related