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.
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.
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.
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
.
ES6 classes are most useful in the following scenarios:
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.
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 |
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
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?
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.
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.
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
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.
console.log(car instanceof Vehicle); // true
console.log(boat instanceof Vehicle); // true
console.log(car instanceof Car); // true
console.log(boat instanceof Car); // false