CHAPTER 9
Classes in JavaScript provide a great way to reuse code. Up until now, there has never been an easier way to support both classes and inheritance in a clean manner. We will take a look at both of these now.
Consider the following Animal class in the code-listing-127.js file:
Code Listing: 127
export class Animal { constructor(name) { this.name = name; } greeting(sound) { return `A ${this.name} ${sound}`; } static echo(msg) { console.log(msg); } } |
Just like functions, we can export classes to be loaded in another module. We are declaring the Animal class. We have a constructor that takes in a single parameter with the name name. Finally, we have a single function called greeting that takes in a single parameter called sound. The greeting function uses the new string interpolation syntax to create a custom message when called.
I am sure you have noticed the static function echo. The static keyword allows us to mark a function as static. This allows us to reference the function via the class name instead of an instance.
Let’s take a look at the AnimalClient class in the code-listing-128.js file:
Code Listing: 128
import {Animal} from './code-listing-127'; export class AnimalClient { constructor() { this.animal = new Animal("Dog"); console.log(this.animal.greeting("barks")); } } let ac = new AnimalClient(); Animal.echo("roof, roof"); |
As we have seen before, we are importing the animal class from the code-listing-126.js file. Next, we are constructing an AnimalClient class that creates a new Animal instance inside its constructor and also renders the greeting function out to the console. Since we are testing this code, we have a single line after the class to kick everything off and get things going as well as a call to the static function echo on the Animal class. Even though we are using AnimalClient from the same file, we export the class in order for it to be accessed from another file.
The following is the output from the preceding code:
Code Listing: 129
A Dog barks roof, roof |
Now that we understand what the class syntax looks like, let’s extend the Animal class and create another one that inherits from it. Consider the following code from the code-listing-129.js file:
Code Listing: 130
export class Animal { constructor(name) { this.name = name; } greeting(sound) { return `A ${this.name} ${sound}`; } static echo(input) { console.log(input); } } export class Canine extends Animal { constructor() { super("canine"); } static echo() { super.echo("bow wow"); } } |
The Animal class hasn’t changed, but we have added another class to the file called Canine. As you might expect, this class extends the Animal class. This passes in the string canine to the base constructor using the super keyword. We can also use super to access functions and properties off of the base class. This is illustrated in the example of the echo function calling the base implementation and passing the string bow wow.
Now let’s look at getters and setters. Consider the Animal class in the code-listing-131.js file:
Code Listing: 131
export class Animal { constructor(name) { this.name = name; this.color = "brown"; } get color() { return this._color; } set color(value) { this._color = value; } toString() { return console.log(`I am a ${this.name}. I am ${this.color} in color.`); } static echo(input) { console.log(input); } } |
As you can see, we are using two new keywords: get and set. This allows us to provide getters and setters by wrapping other variables or performing other operations. This can be very powerful and allows you to centralize all access to your variables.
Let’s look at how this is used in the AnimalClient class in the code-listing-132.js file:
Code Listing: 132
import {Animal} from './code-listing-131'; export class AnimalClient { constructor() { this.animal = new Animal("dog"); this.animal.toString(); } } let ac = new AnimalClient(); |
Finally, here is the output from the preceding code example:
Code Listing: 133
I am a dog. I am brown in color. |
Until ES6, subclassing built-ins has been extremely difficult. Consider the following example that does not use built-ins:
Code Listing: 134
function SuperCtor(arg1) { ... } function SubCtor(arg1, arg2) { SuperCtor.call(this, arg1); } SubCtor.prototype = Object.create(SuperCtor.prototype); |
This is the prototypical way to subclass another object. However, when we deal with built-ins like Array, Date, and DOM Element(s), this is nearly impossible. In JavaScript, if you invoke a built-in constructor, two steps happen internally with every function:
If we were to use the same subclass pattern previously shown, it simply wouldn’t work. However, with ES6, we now have the ability to subclass built-ins.
Object construction for a function named Ctor now uses two phases, both of which are virtually dispatched:
The known @@create symbol is available via Symbol.create. Built-ins now expose @@create explicitly.
Consider the following code example:
Code Listing: 135
// Pseudo-code of Array class Array { constructor(...args) { /* ... */ } static [Symbol.create]() { // Install special [[DefineOwnProperty]] // to magically update 'length' } } // User code of Array subclass class MyArray extends Array { constructor(...args) { super(...args); } } // Two-phase 'new': // 1) Call @@create to allocate object // 2) Invoke constructor on new instance var arr = new MyArray(); arr[1] = 12; console.log(arr.length == 2); console.log(arr); |
The first class is an example of what the native JavaScript Array class would look like. What is important here is understanding that by using the @@create symbol we can extend the built-in class. The code required to create your own Array is trivial, although it does not do anything different.
The following is the output of the preceding code:
Code Listing: 136
true [ , 12 ] |
The comma in the output indicates that the length was indeed 12 by returning the result of evaluating the length in a Boolean expression. It also renders out to the console the array where we can easily see that the first index position has nothing assigned.