Skip to main content

Classes in ECMAScript 2015

5 min read

Older Article

This article was published 9 years ago. Some information may be outdated or no longer applicable.

If you’ve worked with JavaScript before, you’ve probably bumped into the terms “prototypal inheritance” or “prototype-based language.” ES2015 adds the class keyword, but it doesn’t change the prototypal behaviour underneath.

What prototypal inheritance really means: JavaScript has special, hidden properties on objects called [[Prototype]], which either point to null or reference another object. (You can actually poke at this hidden property by calling .__proto__ on your objects.)

Walking through the entire prototype chain is outside the scope of this article. What we need to look at is what happens when you want object-oriented-style functionality in JavaScript.

Let’s say we want to create a Car class. (Classic Programming 101 territory.) A car has properties: maximum speed, number of doors, colour. We define the class with its properties and then instantiate it, creating multiple cars. We can take this further and create trucks that inherit some parent properties while bolting on their own.

JavaScript isn’t a true object-oriented language. When you create a new instance, the functionality doesn’t get copied to the new object (like it would in a true OO language). Instead, it gets linked via a reference chain: the prototype chain.

Let’s see this in action. We’ll build our car object and assign properties. First, the ES5 way, so we understand how this works before jumping to ES2015 classes.

function Car(make, colour, speed) {
  this.make = make;
  this.colour = colour;
  this.speed = speed;
  this.getMaxSpeed = function () {
    return 'Maximum speed is ' + this.speed + 'km/h.';
  };
}

var car1 = new Car('BMW', 'black', 250);
var car2 = new Car('Audi', 'white', 240);

console.log(car1.getMaxSpeed()); // Maximum speed is 250 km/h.

We create a Car function and specify its properties as arguments. Then we spin up two variables, car1 and car2, both instances of Car (via the new keyword). Both hold all the properties defined in Car, so we can call .getMaxSpeed() and also access make and colour.

Now, to represent a truck. We could create another function repeating the same arguments plus adding a truck-specific property. That’s too much typing. Instead, we’ll apply inheritance.

function Truck(make, colour, speed, wheels) {
  Car.call(this, make, colour, speed);
  this.wheels = wheels;
  this.wheelCount = function () {
    return 'This truck has ' + wheels + ' wheels.';
  };
}

let truck = new Truck('black', 'MAN', '80', 6);
console.log(truck.getMaxSpeed(), truck.wheelCount());

Notice we can still call .getMaxSpeed() from the parent, alongside the truck’s own .wheelCount() method.

class

So how does this look in ES2015? Before we jump in, remember: the underlying prototype mechanism doesn’t change.

The class keyword is syntactic sugar on top of the prototype system. It just helps us write simpler code. Let’s rework the previous examples:

class Car {
  constructor(make, colour, speed) {
    this.make = make;
    this.colour = colour;
    this.speed = speed;
  }

  getMaxSpeed() {
    return `Maximum speed is ${this.speed} km/h.`;
  }
}

const car1 = new Car('BMW', 'black', 250);
console.log(car1.getMaxSpeed());

The syntax is clean. We create a class, add a constructor (a special function that runs when the object gets created and initialised), and that’s it. (You can only have one constructor per class.)

Under the hood, the code above gets translated to something very similar to what we wrote earlier:

var Car = (function () {
  function Car(make, colour, speed) {
    this.make = make;
    this.colour = colour;
    this.speed = speed;
  }
  Car.prototype.getMaxSpeed = function () {
    return 'Maximum speed is ' + this.speed + ' km/h.';
  };
  return Car;
})();
var car1 = new Car('BMW', 'black', 250);
console.log(car1.getMaxSpeed()); // Maximum speed is 250 km/h.

extends

With class syntax we can use the extends keyword to create a subclass. We can overwrite parent methods and define new ones in the child class:

class Car {
  constructor(make, colour, speed) {
    this.make = make;
    this.colour = colour;
    this.speed = speed;
  }

  getMaxSpeed() {
    return `Maximum speed is ${this.speed} km/h.`;
  }
}

class Truck extends Car {
  getMaxSpeed() {
    return `Maximum truck speed is ${this.speed} km/h.`;
  }

  getMake() {
    return `This truck is a ${this.make}.`;
  }
}

const car1 = new Car('BMW', 'black', 250);
const truck = new Truck('MAN', 'black', 80);

console.log(car1.getMaxSpeed()); // Maximum speed is 250 km/h
console.log(truck.getMaxSpeed()); // Maximum truck speed is 80 km/h.
console.log(truck.getMake()); // This truck is a MAN.

super

Sometimes you don’t want to overwrite a parent method. You want to call it. That’s where super comes in:

class Car {
  constructor(make, colour, speed) {
    this.make = make;
    this.colour = colour;
    this.speed = speed;
  }

  getMaxSpeed() {
    return `Maximum speed is ${this.speed} km/h.`;
  }
}

class Truck extends Car {
  getMaxSpeed() {
    console.log(super.getMaxSpeed()); // Maximum speed is 80 km/h.
    return `This truck goes with ${this.speed} km/h.`;
  }
}

const truck = new Truck('MAN', 'black', 80);
console.log(truck.getMaxSpeed()); // This truck goes with 80 km/h.

Another use: appending properties in a child class.

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

  introduce() {
    return `Hello ${this.name}`;
  }
}

class SuperHero extends Person {
  constructor(name, power) {
    super(name);
    this.power = power;
  }

  introduce() {
    return `${super.introduce()}. Your superpower: ${this.power}`;
  }
}

const dave = new SuperHero('Dave', 'invisibility');
console.log(
  dave.introduce() // Hello Dave. Your superpower: invisibility
);

Here we extend Person, a class that takes a name on construction. When we create a SuperHero, we want both name and power as class members. The only way to wire that up is by calling super() in the SuperHero constructor. Notice we also call the parent’s .introduce() method to include the original greeting.