Classes

ES6 classes provide a simplified model for object-oriented programming in JavaScript compared to JavaScript's prototype-based type system. The class syntax makes it easier to use and understand OO in JavaScript programs, especially for developers learning JavaScript after working with traditional OOP languages.

In this chapter, we'll explore how to define and use classes in JavaScript. In a later chapter, we'll discuss JavaScript's prototype-based type system further.

The class syntax does not introduce a new object-oriented inheritance model to JavaScript. Instead, it simply provides some syntactic sugar on top of the prototype system. In a later chapter, we'll see how classes relate to the prototype system.

Defining Classes

In the previous chapter, we defined an object factory for creating objects that represent cats:

function createCat(name, color, age) {
  return {
    name,
    color,
    age,
    speak() {
      console.log(
        `Meow. I am ${name}. ` +
        `I am a ${age}-year-old ${color} cat.`
      );
    }
  };
}

let cocoa = createCat("Cocoa", "black", 5);
let leo = createCat("Leo", "orange", 3);

cocoa.speak();
// Meow. I am Cocoa. I am a 5-year-old black cat.

leo.speak();
// Meow. I am Leo. I am a 3-year-old orange cat.

This code is relatively easy to understand, but it has some disadvantages. For one thing, if cocoa ends up in some other part of the program, you may be unable to determine that it represents a cat:

console.log(cocoa);
// { name: 'Cocoa', color: 'black', age: 5,
//   speak: [Function: speak] }

console.log(typeof cocoa);
// object

console.log(cocoa instanceof createCat);
// false

console.log(cocoa instanceof Cat);
// ReferenceError: Cat is not defined

None of that is helpful, especially if you're debugging and don't know if you have a cat, a dog, a child's soccer team, or just some arbitrary data.

Furthermore, you may need more memory if you have many cats. Every time you run createCat, it creates a new object that includes the data and copies of all its methods. That's likely okay in this program. However, if you're reading a list of a million cats from a file, you might run into problems.

Implementing inheritance relationships with factory functions is possible but could be more elegant.

Classes solve all of these problems.

Here's how we can convert this code to use classes:

class Cat {
  constructor(name, color, age) {
    this.name = name;
    this.color = color;
    this.age = age;
  }

  speak() {
    console.log(
      `Meow. I am ${this.name}. ` +
      `I am a ${this.age}-year-old ${this.color} cat.`
    );
  }
}

let cocoa = new Cat("Cocoa", "black", 5);
let leo = new Cat("Leo", "orange", 3);

cocoa.speak();
// Meow. I am Cocoa. I am a 5-year-old black cat.

leo.speak();
// Meow. I am Leo. I am a 3-year-old orange cat.

console.log(cocoa);
// Cat { name: 'Cocoa', color: 'black', age: 5 }

console.log(cocoa instanceof Cat);
// true

We've created the Cat class with the class keyword. Note that the class name, Cat, uses PascalCase.

In the body of the class (the part between the { and } characters on lines 1 and 14), we have two methods: constructor and speak.

Most classes need a constructor method. JavaScript invokes this method when you call the constructor for the Cat class:

let cocoa = new Cat("Cocoa", "black", 5);

The new keyword tells JavaScript that it should instantiate a new Cat object by calling the constructor method for class Cat. The new keyword initially creates a new empty object, sets the value of this in constructor to reference the new object, and then calls constructor with the specified arguments. Finally, JavaScript returns the completed object to the caller when constructor finishes running.

The constructor usually initializes all the properties the object will eventually use. In this case, we initialize the name, color, and age properties. Note that you must use this to reference the property names in this method.

You must also use this to access properties in any other methods you define in the class.

Class Inheritance

Inheritance is a way to form new classes using existing classes. The new class (called the subclass) can inherit properties and methods from the original class (called the superclass).

For instance, assume we have a Rectangle class that lets us define rectangular objects:

class Rectangle {
  constructor(height, width) {
    this.height = height;
    this.width = width;
  }

  area() {
    return this.height * this.width;
  }
}

const myRectangle = new Rectangle(10, 5);
console.log(myRectangle.area());        // 50

Now, assume we also want a Square class to define square objects:

class Square {
  constructor(side) {
    this.side = side;
  }

  area() {
    return this.side * this.side;
  }
}

const mySquare = new Square(6);
console.log(mySquare.area());           // 36

This code works, but let's think about it. In mathematics, a square is a particular case of a rectangle whose width and height are identical: a Square is a type of Rectangle. That implies an "is a" relationship between squares and rectangles. In OOP, an "is a" relationship typically means that inheritance may be appropriate. So, let's make the Square class inherit from the Rectangle class:

class Rectangle {
  constructor(height, width) {
    this.height = height;
    this.width = width;
  }

  area() {
    return this.height * this.width;
  }
}

const myRectangle = new Rectangle(10, 5);
console.log(myRectangle.area());        // 50

//highlight
class Square extends Rectangle {
  constructor(side) {
    super(side, side);
    this.side = side;
  }
}
//endhighlight

const mySquare = new Square(6);
console.log(mySquare.area());           // 36
console.log(mySquare.side);             // 6

We've modified the Square class by adding extends Rectangle to the first line. The extends keyword tells JavaScript that the current class (Square) will inherit from the class named by the extends clause (Rectangle). Objects of type Square will be able to access and use all of the properties and methods of the Rectangle class. Since Square inherits Rectangle's methods, we don't need to define area in the Square class.

In OO parlance, we often talk of the inheriting class being a subclass of the inherited class and the inherited class as the superclass of the inheriting class. Thus, Square is a subclass of Rectangle, and Rectangle is a superclass of Square.

The constructor method usually needs more care in the inheriting class. Typically, the first thing you want to do in the inheriting class's constructor method is to call super; super tells JavaScript that it needs to call the same method (constructor, in this case) in the superclass (Rectangle).

We don't have to do it here, but we've saved side as a property in the Square class. Thus, we can determine the original side value by referencing the size property.

Note that you should usually call super() before you do anything else in a constructor.

You can also use super to call methods other than constructor in the superclass, though the syntax is a little different:

class Parent {
  whatMethod() {
    console.log('In the parent method');
  }
}

class Child extends Parent {
  whatMethod() {
    console.log('In the child method');
    super.whatMethod(); // <-- calling `whatMethod` in `Parent` class.
    console.log('Back in the child method');
  }
}

let child = new Child();
child.whatMethod();
// In the child method
// In the parent method
// Back in the child method

Finally, let's see what happens when we try to determine the type of objects in a class hierarchy:

console.log(myRectangle); // Rectangle { height: 10, width: 5 }
console.log(mySquare);    // Square { height: 6, width: 6, side: 6 }

console.log(mySquare instanceof Square);          // true
console.log(mySquare instanceof Rectangle);       // true

console.log(myRectangle instanceof Square);       // false
console.log(myRectangle instanceof Rectangle);    // true

Note that a Square instance object is an instance of both the Square and Rectangle classes. That's because squares are rectangles in this hierarchy. However, a Rectangle instance object is not an instance of the Square class.

Inheritance Hierarchies

Note that inheritance isn't limited to just a single superclass and subclass. When inheritance is part of a program, we often talk of inheritance hierarchies. An inheritance hierarchy refers to relationships between related classes. It represents the parent-child relationships among classes, where a child class (also known as a subclass or derived class) inherits from a parent class (also known as a superclass or base class).

Consider the following hierarchy diagram in which classes B and C inherit from class A, classes D and E inherit from class B, and classes F and G inherit from class C:

                     +---------+
                     | Class A |
                     +----+----+
                          |
            +-------------+-------------+
            |                           |
       +----+----+                 +----+----+
       | Class B |                 | Class C |
       +----+----+                 +----+----+
            |                           |
     +------+------+             +------+------+
     |             |             |             |
+----+----+   +----+----+   +----+----+   +----+----+
| Class D |   | Class E |   | Class F |   | Class G |
+---------+   +---------+   +---------+   +---------+

Class A is the parent (an immediate superclass) of classes B and C. However, it is also considered a superclass (or ancestor class) of classes D, E, F, and G. Furthermore, class B is the parent of classes D and E, and class C is the parent of classes F and G.

Meanwhile, classes D and E are the children (an immediate subclass) of class B, classes F and G are the children of class C, and classes B and C are the children of class A. We can also speak of classes D, E, F, and G as descendants of class A.

Using terms like parent, child, ancestor, and descendant can be further extended to other familial relationships. For instance, classes D and E are siblings, while classes F and G are cousins of class D.

When Should You Use ES6 Classes?

ES6 classes are most useful in the following scenarios:

  • When you need inheritance.
  • When your inheritance needs are complex.
  • When you're interested in writing readable OO code.
  • When your team prefers to use classes.
  • When you need many objects with methods.
  • When the types of your objects are essential to your application.
  • When your application already uses classes.
  • When writing code meant for use with TypeScript.

Summary

Classes in JavaScript provide a clear, simple syntax for creating objects and dealing with inheritance. While they are syntactical sugar over JavaScript's existing prototype-based model, they offer a more intuitive and accessible way to approach object-oriented programming in JavaScript.

Exercises

  1. This exercise re-examines exercise 2 from the previous chapter. In that exercise, you wrote an object factory to instantiate objects that represent smartphones. In this exercise, we'll rewrite that factory using a class.

    Write a class that can be used to instantiate objects that represent smartphones. Each smartphone should have a brand, model, and release year. Add methods to check the battery level and to display the smartphone's information. Create objects that represent the following 2 smartphones:

    Brand Model Release Year
    Apple iPhone 12 2020
    Samsung Galaxy S21 2021

    Solution

    class Smartphone {
      constructor(brand, model, releaseYear) {
        this.brand = brand;
        this.model = model;
        this.releaseYear = releaseYear;
      }
    
      checkBatteryLevel() {
        return `${this.brand} ${this.model} has 75% battery remaining.`;
      }
    
      displayInfo() {
        return `${this.releaseYear} ${this.brand} ${this.model}`;
      }
    }
    
    let iphone12 = new Smartphone('Apple', 'iPhone 12', 2020);
    let galaxyS21 = new Smartphone('Samsung', 'Galaxy S21', 2021);
    
    console.log(iphone12.checkBatteryLevel());
    // Apple iPhone 12 has 75% battery remaining.
    
    console.log(iphone12.displayInfo());
    // 2020 Apple iPhone 12
    
    console.log(galaxyS21.checkBatteryLevel());
    // Samsung Galaxy S21 has 75% battery remaining.
    
    console.log(galaxyS21.displayInfo());
    // 2021 Samsung Galaxy S21
    
  2. If you have a Dog class and an object assigned to a variable named boo, how can you tell whether the object is an instance of the Dog class?

    Solution

    console.log(boo instanceof Dog);
    // true if `boo` is a Dog, false if it is not
    
    console.log(boo);
    // Dog { name: 'Boo', color: 'chocolate', age: 3 }
    //
    // Your JavaScript engine may show different output,
    // but it should clearly show that you are looking
    // at a Dog.
    
  3. Create a class hierarchy consisting of vehicles, including cars, boats, and planes, as specific kinds of vehicles. All vehicles should be able to accelerate and decelerate. Cars should be able to honk, boats should be able to drop anchor, and planes should be able to take off and land. Test your code.

    All vehicles should have a color and weight. Cars have a license number, boats have a home port, and planes have an airline name.

    Solution

    class Vehicle {
      constructor(color, weight) {
        this.color = color;
        this.weight = weight;
      }
    
      accelerate() {
        console.log("Accelerate");
      }
    
      decelerate() {
        console.log("Decelerate");
      }
    }
    
    class Car extends Vehicle {
      constructor(color, weight, licenseNumber) {
        super(color, weight);
        this.licenseNumber = licenseNumber;
      }
    
      honk() {
        console.log("Honk");
      }
    }
    
    class Boat extends Vehicle {
      constructor(color, weight, homePort) {
        super(color, weight);
        this.homePort = homePort;
      }
    
      dropAnchor() {
        console.log("Drop anchor");
      }
    }
    
    class Plane extends Vehicle {
      constructor(color, weight, airline) {
        super(color, weight);
        this.airline = airline;
      }
    
      takeOff() {
        console.log("Take off");
      }
    
      land() {
        console.log("Land");
      }
    }
    
    let car = new Car('red', 3300, 'BXY334');
    car.accelerate();             // Accelerate
    car.honk();                   // Honk
    car.decelerate();             // Decelerate
    console.log(car.color, car.weight, car.licenseNumber);
    // red 3300 BXY334
    
    let boat = new Boat('yellow', 12000, 'Bahamas');
    boat.accelerate();            // Accelerate
    boat.decelerate();            // Decelerate
    boat.dropAnchor();            // Drop anchor
    console.log(boat.color, boat.weight, boat.homePort);
    // yellow 12000 Bahamas
    
    let plane = new Plane('blue', 83000, 'Southwest');
    plane.accelerate();           // Accelerate
    plane.takeOff();              // Take off
    plane.land();                 // Land
    plane.decelerate();           // Decelerate
    console.log(plane.color, plane.weight, plane.airline);
    // blue 83000 Southwest
    
  4. Using the solution to the previous exercise, demonstrate that cars and boats are both instance objects of the Vehicle class, that cars are instance objects of the Car class, but boats are not instance objects of the Car class.

    Solution

    console.log(car instanceof Vehicle);    // true
    console.log(boat instanceof Vehicle);   // true
    
    console.log(car instanceof Car);        // true
    console.log(boat instanceof Car);       // false