ES2015 Lesson 4: Classes


The concept of prototypes and prototypal inheritance in ES5 are hard to understand for many developers transitioning from another programming language to JavaScript.

ES6 classes introduce syntactic sugar to make prototypes look like classical inheritance.

For this reason, some people applaud classes, as it makes JavaScript appear more familiar to them.

Others seem to have launched a holy war against classes, claiming that the class syntax is flawed.

On some level, all opinions have merit. My advice to you is that the market is always right. Knowing classes gives you the advantage that you can maintain code written in the class syntax. It does not mean that you have to use it. If your judgement justifies that classes should be used, go for it.

I personally use classes on a regular basis, and my React-Redux tutorials also make use of the class syntax.

Not knowing the class syntax is a disadvantage.

Judgement on the class syntax, or offering alternatives are beyond the scope of this section.

Prototypal Inheritance in ES5

Let’s start with an example, where we implement a classical inheritance scenario in JavaScript.

Rectangle is a constructor function. Even though there were no classes in ES5, many people called constructor functions and their prototype extensions classes.

We instantiate a class with the new keyword, creating an object out of it. In ES5 terminology, constructor functions return new objects, having defined of properties and operations.

Prototypal inheritance is defined between Shape and Rectangle, as a rectangle is a shape. Therefore, we can call the getColor method on a rectangle, even though it is defined for shapes.

Prototypal inheritance is implicitly defined between Object and Shape. As the prototype chain is transitive, we can call the toString built-in method on a rectangle object, even though it comes from the prototype of Object.

The code looks a bit weird, and chunks of code that should belong together are separated.

The ES6 way

Let’s see the ES6 version. As we’ll see later, the two versions are not equivalent, we just describe the same problem domain with ES6 code.

Classes may encapsulate

  • a constructor function
  • additional operations extending the prototype
  • reference to the parent prototype.

Notice the following:

  • the extends keyword defines the is-a relationship between Shape and Rectangle. All instances of Rectangle are also instances of Shape.
  • the constructor method is a method that runs when you instantiate a class. You can call the constructor method of your parent class with super. More on super later.
  • methods can be defined inside classes. All objects are able to call methods of their class and all classes that are higher in the inheritance chain.
  • Instantiation works in the exact same way as the instantiation of an ES5 constructor function.
  • The methods are written using the concise method syntax. We will learn about this syntax in depth in Chapter 7 of ES6 in Practice.

You can observe the equivalent ES5 code by pasting the above code into the BabelJs online editor.

The reason why the generated code is not equivalent with the ES5 code we studied is that the ES6 class syntax comes with additional features. You will never need the protection provided by these features during regular use. For instance, if you call the class name as a regular function, or you call a method of the class with the new operator as a constructor, you get an error.

Your code becomes more readable, when you capitalize class names, and start object names and method names with a lower case letter. For instance, Person should be a class, and person should be an object.

Super

Calling super in a constructor should happen before accessing this. As a rule of thumb:

Call super as the first thing in a constructor of a class defined with extends.

If you fail to call super, an error will be thrown. If you don’t define a constructor in a class defined with extends, one will automatically be created for you, calling super.

Shadowing

Methods of the parent class can be redefined in the child class.

Creating abstract classes

Abstract classes are classes that cannot be instantiated. Recall the Shape class in the previous example. Until we know what kind of shape we are talking about, we cannot do much with a generic shape.

Often times, you have a couple of business objects on the same level. Assuming that you are not in the WET (We Enjoy Typing) group of developers, it is natural that you abstract the common functionalities into a base class. For instance, in case of stock trading, you may have a BarChartView, a LineChartView, and a CandlestickChartView. The common functionalities related to these three views are abstracted into a ChartView. If you want to make ChartView abstract, do the following:

The built-in property new.target contains a reference to the class written next to the new keyword during instantiation. This is the name of the class whose constructor was first called in the inheritance chain.

Getters and Setters

Getters and setters are used to create computed properties.

In the below example, I will use > to indicate the response of an expression. Feel free to experiment with the below classes using your Chrome console.

Note that area only has a getter. Setting area does not change anything, as area is a computed property that depends on the width of the square.

For the sake of demonstrating setters, let’s define a height computed property.

Width and height can be used as regular properties of a Square object, and the two values are kept in sync using the height getter and setter.

Advantages of getters and setters:

  • Elimination of redundancy: computed fields can be derived using an algorithm depending on other properties.
  • Information hiding: do not expose properties that are retrievable or settable through getters or setters.
  • Encapsulation: couple other functionality with getting/setting a value.
  • Defining a public interface: keep these definitions constant and reliable, while you are free to change the internal representation used for computing these fields. This comes handy e.g. when dealing with a DOM structure, where the template may change
  • Easier debugging: just add debugging commands or breakpoints to a setter, and you will know what caused a value to change.

Static methods

Static methods are operations defined on a class. These methods can only be referenced from the class itself, not from objects.

Exercises

Exercise 1. Create a PlayerCharacter and a NonPlayerCharacter with a common ancestor Character. The characters are located in a 10×10 game field. All characters appear at a random location. Create the three classes, and make sure you can query where each character is. (Solution)


Exercise 2. Each character has a direction (up, down, left, right). Player characters initially go right, and their direction can be changed using the faceUp, faceDown, faceLeft, faceRight methods. Non-player characters move randomly. A move is automatically taken every 5 seconds in real time. Right after the synchronized moves, each character console logs its position. The player character can only influence the direction he is facing. When a player meets a non-player character, the non-player character is eliminated from the game, and the player’s score is increased by 1. (Solution)


Exercise 3. Make sure the Character class cannot be instantiated. (Solution)