Inheritance with Derived Classes
Prior to ECMAScript 6, implementing inheritance with custom types was an extensive process. Proper inheritance required multiple steps. For instance, consider this example:
function Rectangle(length, width) {
this.length = length;
this.width = width;
}
Rectangle.prototype.getArea = function() {
return this.length * this.width;
};
function Square(length) {
Rectangle.call(this, length, length);
}
Square.prototype = Object.create(Rectangle.prototype, {
constructor: {
value:Square,
enumerable: false,
writable: true,
configurable: true
}
});
var square = new Square(3);
console.log(square.getArea()); // 9
console.log(square instanceof Square); // true
console.log(square instanceof Rectangle); // true
Square
inherits from Rectangle
, and to do so, it must overwrite Square.prototype
with a new object created from Rectangle.prototype
as well as call the Rectangle.call()
method. These steps often confused JavaScript newcomers and were a source of errors for experienced developers.
Classes make inheritance easier to implement by using the familiar extends
keyword to specify the function from which the class should inherit. The prototypes are automatically adjusted, and you can access the base class constructor by calling the super()
method. Here’s the ECMAScript 6 equivalent of the previous example:
class Rectangle {
constructor(length, width) {
this.length = length;
this.width = width;
}
getArea() {
return this.length * this.width;
}
}
class Square extends Rectangle {
constructor(length) {
// same as Rectangle.call(this, length, length)
super(length, length);
}
}
var square = new Square(3);
console.log(square.getArea()); // 9
console.log(square instanceof Square); // true
console.log(square instanceof Rectangle); // true
This time, the Square
class inherits from Rectangle
using the extends
keyword. The Square
constructor uses super()
to call the Rectangle
constructor with the specified arguments. Note that unlike the ECMAScript 5 version of the code, the identifier Rectangle
is only used within the class declaration (after extends
).
Classes that inherit from other classes are referred to as derived classes. Derived classes require you to use super()
if you specify a constructor; if you don’t, an error will occur. If you choose not to use a constructor, then super()
is automatically called for you with all arguments upon creating a new instance of the class. For instance, the following two classes are identical:
class Square extends Rectangle {
// no constructor
}
// Is equivalent to
class Square extends Rectangle {
constructor(...args) {
super(...args);
}
}
The second class in this example shows the equivalent of the default constructor for all derived classes. All of the arguments are passed, in order, to the base class constructor. In this case, the functionality isn’t quite correct because the Square
constructor needs only one argument, and so it’s best to manually define the constructor.
W> There are a few things to keep in mind when using super()
: W> W> 1. You can only use super()
in a derived class. If you try to use it in a non-derived class (a class that doesn’t use extends
) or a function, it will throw an error. W> 1. You must call super()
before accessing this
in the constructor. Since super()
is responsible for initializing this
, attempting to access this
before calling super()
results in an error. W> 1. The only way to avoid calling super()
is to return an object from the class constructor.
Shadowing Class Methods
The methods on derived classes always shadow methods of the same name on the base class. For instance, you can add getArea()
to Square
to redefine that functionality:
class Square extends Rectangle {
constructor(length) {
super(length, length);
}
// override and shadow Rectangle.prototype.getArea()
getArea() {
return this.length * this.length;
}
}
Since getArea()
is defined as part of Square
, the Rectangle.prototype.getArea()
method will no longer be called by any instances of Square
. Of course, you can always decide to call the base class version of the method by using the super.getArea()
method, like this:
class Square extends Rectangle {
constructor(length) {
super(length, length);
}
// override, shadow, and call Rectangle.prototype.getArea()
getArea() {
return super.getArea();
}
}
Using super
in this way works the same as the the super references discussed in Chapter 4 (see “Easy Prototype Access With Super References”). The this
value is automatically set correctly so you can make a simple method call.
Inherited Static Members
If a base class has static members, then those static members are also available on the derived class. Inheritance works like that in other languages, but this is a new concept for JavaScript. Here’s an example:
class Rectangle {
constructor(length, width) {
this.length = length;
this.width = width;
}
getArea() {
return this.length * this.width;
}
static create(length, width) {
return new Rectangle(length, width);
}
}
class Square extends Rectangle {
constructor(length) {
// same as Rectangle.call(this, length, length)
super(length, length);
}
}
var rect = Square.create(3, 4);
console.log(rect instanceof Rectangle); // true
console.log(rect.getArea()); // 12
console.log(rect instanceof Square); // false
In this code, a new static create()
method is added to the Rectangle
class. Through inheritance, that method is available as Square.create()
and behaves in the same manner as the Rectangle.create()
method.
Derived Classes from Expressions
Perhaps the most powerful aspect of derived classes in ECMAScript 6 is the ability to derive a class from an expression. You can use extends
with any expression as long as the expression resolves to a function with [[Construct]]
and a prototype. For instance:
function Rectangle(length, width) {
this.length = length;
this.width = width;
}
Rectangle.prototype.getArea = function() {
return this.length * this.width;
};
class Square extends Rectangle {
constructor(length) {
super(length, length);
}
}
var x = new Square(3);
console.log(x.getArea()); // 9
console.log(x instanceof Rectangle); // true
Rectangle
is defined as an ECMAScript 5-style constructor while Square
is a class. Since Rectangle
has [[Construct]]
and a prototype, the Square
class can still inherit directly from it.
Accepting any type of expression after extends
offers powerful possibilities, such as dynamically determining what to inherit from. For example:
function Rectangle(length, width) {
this.length = length;
this.width = width;
}
Rectangle.prototype.getArea = function() {
return this.length * this.width;
};
function getBase() {
return Rectangle;
}
class Square extends getBase() {
constructor(length) {
super(length, length);
}
}
var x = new Square(3);
console.log(x.getArea()); // 9
console.log(x instanceof Rectangle); // true
The getBase()
function is called directly as part of the class declaration. It returns Rectangle
, making this example is functionally equivalent to the previous one. And since you can determine the base class dynamically, it’s possible to create different inheritance approaches. For instance, you can effectively create mixins:
let SerializableMixin = {
serialize() {
return JSON.stringify(this);
}
};
let AreaMixin = {
getArea() {
return this.length * this.width;
}
};
function mixin(...mixins) {
var base = function() {};
Object.assign(base.prototype, ...mixins);
return base;
}
class Square extends mixin(AreaMixin, SerializableMixin) {
constructor(length) {
super();
this.length = length;
this.width = length;
}
}
var x = new Square(3);
console.log(x.getArea()); // 9
console.log(x.serialize()); // "{"length":3,"width":3}"
In this example, mixins are used instead of classical inheritance. The mixin()
function takes any number of arguments that represent mixin objects. It creates a function called base
and assigns the properties of each mixin object to the prototype. The function is then returned so Square
can use extends
. Keep in mind that since extends
is still used, you are required to call super()
in the constructor.
The instance of Square
has both getArea()
from AreaMixin
and serialize
from SerializableMixin
. This is accomplished through prototypal inheritance. The mixin()
function dynamically populates the prototype of a new function with all of the own properties of each mixin. (Keep in mind that if multiple mixins have the same property, only the last property added will remain.)
W> Any expression can be used after extends
, but not all expressions result in a valid class. Specifically, the following expression types cause errors: W> W> * null
W> * generator functions (covered in Chapter 8) W> W> In these cases, attempting to create a new instance of the class will throw an error because there is no [[Construct]]
to call.
Inheriting from Built-ins
For almost as long as JavaScript arrays have existed, developers have wanted to create their own special array types through inheritance. In ECMAScript 5 and earlier, this wasn’t possible. Attempting to use classical inheritance didn’t result in functioning code. For example:
// built-in array behavior
var colors = [];
colors[0] = "red";
console.log(colors.length); // 1
colors.length = 0;
console.log(colors[0]); // undefined
// trying to inherit from array in ES5
function MyArray() {
Array.apply(this, arguments);
}
MyArray.prototype = Object.create(Array.prototype, {
constructor: {
value: MyArray,
writable: true,
configurable: true,
enumerable: true
}
});
var colors = new MyArray();
colors[0] = "red";
console.log(colors.length); // 0
colors.length = 0;
console.log(colors[0]); // "red"
The console.log()
output at the end of this code shows how using the classical form of JavaScript inheritance on an array results in unexpected behavior. The length
and numeric properties on an instance of MyArray
don’t behave the same as they do for the built-in array because this functionality isn’t covered either by Array.apply()
or by assigning the prototype.
One goal of ECMAScript 6 classes is to allow inheritance from all built-ins. In order to accomplish this, the inheritance model of classes is slightly different than the classical inheritance model found in ECMAScript 5 and earlier:
In ECMAScript 5 classical inheritance, the value of this
is first created by the derived type (for example, MyArray
), and then the base type constructor (like the Array.apply()
method) is called. That means this
starts out as an instance of MyArray
and then is decorated with additional properties from Array
.
In ECMAScript 6 class-based inheritance, the value of this
is first created by the base (Array
) and then modified by the derived class constructor (MyArray
). The result is that this
starts with all the built-in functionality of the base and correctly receives all functionality related to it.
The following example shows a class-based special array in action:
class MyArray extends Array {
// empty
}
var colors = new MyArray();
colors[0] = "red";
console.log(colors.length); // 1
colors.length = 0;
console.log(colors[0]); // undefined
MyArray
inherits directly from Array
and therefore works like Array
. Interacting with numeric properties updates the length
property, and manipulating the length
property updates the numeric properties. That means you can both properly inherit from Array
to create your own derived array classes and inherit from other built-ins as well. With all this added functionality, ECMAScript 6 and derived classes have effectively removed the last special case of inheriting from built-ins, but that case is still worth exploring.
The Symbol.species Property
An interesting aspect of inheriting from built-ins is that any method that returns an instance of the built-in will automatically return a derived class instance instead. So, if you have a derived class called MyArray
that inherits from Array
, methods such as slice()
return an instance of MyArray
. For example:
class MyArray extends Array {
// empty
}
let items = new MyArray(1, 2, 3, 4),
subitems = items.slice(1, 3);
console.log(items instanceof MyArray); // true
console.log(subitems instanceof MyArray); // true
In this code, the slice()
method returns a MyArray
instance. The slice()
method is inherited from Array
and returns an instance of Array
normally. However, the constructor for the return value is read from the Symbol.species
property, allowing for this change.
The Symbol.species
well-known symbol is used to define a static accessor property that returns a function. That function is a constructor to use whenever an instance of the class must be created inside of an instance method (instead of using the constructor). The following builtin types have Symbol.species
defined:
Array
ArrayBuffer
(discussed in Chapter 10)Map
Promise
RegExp
Set
- Typed Arrays (discussed in Chapter 10)
Each of these types has a default Symbol.species
property that returns this
, meaning the property will always return the constructor function. If you were to implement that functionality on a custom class, the code would look like this:
// several builtin types use species similar to this
class MyClass {
static get [Symbol.species]() {
return this;
}
constructor(value) {
this.value = value;
}
clone() {
return new this.constructor[Symbol.species](this.value);
}
}
In this example, the Symbol.species
well-known symbol is used to assign a static accessor property to MyClass
. Note that there’s only a getter without a setter, because changing the species of a class isn’t possible. Any call to this.constructor[Symbol.species]
returns MyClass
. The clone()
method uses that definition to return a new instance rather than directly using MyClass
, which allows derived classes to override that value. For example:
class MyClass {
static get [Symbol.species]() {
return this;
}
constructor(value) {
this.value = value;
}
clone() {
return new this.constructor[Symbol.species](this.value);
}
}
class MyDerivedClass1 extends MyClass {
// empty
}
class MyDerivedClass2 extends MyClass {
static get [Symbol.species]() {
return MyClass;
}
}
let instance1 = new MyDerivedClass1("foo"),
clone1 = instance1.clone(),
instance2 = new MyDerivedClass2("bar"),
clone2 = instance2.clone();
console.log(clone1 instanceof MyClass); // true
console.log(clone1 instanceof MyDerivedClass1); // true
console.log(clone2 instanceof MyClass); // true
console.log(clone2 instanceof MyDerivedClass2); // false
Here, MyDerivedClass1
inherits from MyClass
and doesn’t change the Symbol.species
property. When clone()
is called, it returns an instance of MyDerivedClass1
because this.constructor[Symbol.species]
returns MyDerivedClass1
. The MyDerivedClass2
class inherits from MyClass
and overrides Symbol.species
to return MyClass
. When clone()
is called on an instance of MyDerivedClass2
, the return value is an instance of MyClass
. Using Symbol.species
, any derived class can determine what type of value should be returned when a method returns an instance.
For instance, Array
uses Symbol.species
to specify the class to use for methods that return an array. In a class derived from Array
, you can determine the type of object returned from the inherited methods, such as:
class MyArray extends Array {
static get [Symbol.species]() {
return Array;
}
}
let items = new MyArray(1, 2, 3, 4),
subitems = items.slice(1, 3);
console.log(items instanceof MyArray); // true
console.log(subitems instanceof Array); // true
console.log(subitems instanceof MyArray); // false
This code overrides Symbol.species
on MyArray
, which inherits from Array
. All of the inherited methods that return arrays will now use an instance of Array
instead of MyArray
.
In general, you should use the Symbol.species
property whenever you might want to use this.constructor
in a class method. Doing so allows derived classes to override the return type easily. Additionally, if you are creating derived classes from a class that has Symbol.species
defined, be sure to use that value instead of the constructor.