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:

  1. function Rectangle(length, width) {
  2. this.length = length;
  3. this.width = width;
  4. }
  5. Rectangle.prototype.getArea = function() {
  6. return this.length * this.width;
  7. };
  8. function Square(length) {
  9. Rectangle.call(this, length, length);
  10. }
  11. Square.prototype = Object.create(Rectangle.prototype, {
  12. constructor: {
  13. value:Square,
  14. enumerable: false,
  15. writable: true,
  16. configurable: true
  17. }
  18. });
  19. var square = new Square(3);
  20. console.log(square.getArea()); // 9
  21. console.log(square instanceof Square); // true
  22. 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:

  1. class Rectangle {
  2. constructor(length, width) {
  3. this.length = length;
  4. this.width = width;
  5. }
  6. getArea() {
  7. return this.length * this.width;
  8. }
  9. }
  10. class Square extends Rectangle {
  11. constructor(length) {
  12. // same as Rectangle.call(this, length, length)
  13. super(length, length);
  14. }
  15. }
  16. var square = new Square(3);
  17. console.log(square.getArea()); // 9
  18. console.log(square instanceof Square); // true
  19. 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:

  1. class Square extends Rectangle {
  2. // no constructor
  3. }
  4. // Is equivalent to
  5. class Square extends Rectangle {
  6. constructor(...args) {
  7. super(...args);
  8. }
  9. }

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:

  1. class Square extends Rectangle {
  2. constructor(length) {
  3. super(length, length);
  4. }
  5. // override and shadow Rectangle.prototype.getArea()
  6. getArea() {
  7. return this.length * this.length;
  8. }
  9. }

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:

  1. class Square extends Rectangle {
  2. constructor(length) {
  3. super(length, length);
  4. }
  5. // override, shadow, and call Rectangle.prototype.getArea()
  6. getArea() {
  7. return super.getArea();
  8. }
  9. }

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:

  1. class Rectangle {
  2. constructor(length, width) {
  3. this.length = length;
  4. this.width = width;
  5. }
  6. getArea() {
  7. return this.length * this.width;
  8. }
  9. static create(length, width) {
  10. return new Rectangle(length, width);
  11. }
  12. }
  13. class Square extends Rectangle {
  14. constructor(length) {
  15. // same as Rectangle.call(this, length, length)
  16. super(length, length);
  17. }
  18. }
  19. var rect = Square.create(3, 4);
  20. console.log(rect instanceof Rectangle); // true
  21. console.log(rect.getArea()); // 12
  22. 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:

  1. function Rectangle(length, width) {
  2. this.length = length;
  3. this.width = width;
  4. }
  5. Rectangle.prototype.getArea = function() {
  6. return this.length * this.width;
  7. };
  8. class Square extends Rectangle {
  9. constructor(length) {
  10. super(length, length);
  11. }
  12. }
  13. var x = new Square(3);
  14. console.log(x.getArea()); // 9
  15. 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:

  1. function Rectangle(length, width) {
  2. this.length = length;
  3. this.width = width;
  4. }
  5. Rectangle.prototype.getArea = function() {
  6. return this.length * this.width;
  7. };
  8. function getBase() {
  9. return Rectangle;
  10. }
  11. class Square extends getBase() {
  12. constructor(length) {
  13. super(length, length);
  14. }
  15. }
  16. var x = new Square(3);
  17. console.log(x.getArea()); // 9
  18. 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:

  1. let SerializableMixin = {
  2. serialize() {
  3. return JSON.stringify(this);
  4. }
  5. };
  6. let AreaMixin = {
  7. getArea() {
  8. return this.length * this.width;
  9. }
  10. };
  11. function mixin(...mixins) {
  12. var base = function() {};
  13. Object.assign(base.prototype, ...mixins);
  14. return base;
  15. }
  16. class Square extends mixin(AreaMixin, SerializableMixin) {
  17. constructor(length) {
  18. super();
  19. this.length = length;
  20. this.width = length;
  21. }
  22. }
  23. var x = new Square(3);
  24. console.log(x.getArea()); // 9
  25. 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:

  1. // built-in array behavior
  2. var colors = [];
  3. colors[0] = "red";
  4. console.log(colors.length); // 1
  5. colors.length = 0;
  6. console.log(colors[0]); // undefined
  7. // trying to inherit from array in ES5
  8. function MyArray() {
  9. Array.apply(this, arguments);
  10. }
  11. MyArray.prototype = Object.create(Array.prototype, {
  12. constructor: {
  13. value: MyArray,
  14. writable: true,
  15. configurable: true,
  16. enumerable: true
  17. }
  18. });
  19. var colors = new MyArray();
  20. colors[0] = "red";
  21. console.log(colors.length); // 0
  22. colors.length = 0;
  23. 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:

  1. class MyArray extends Array {
  2. // empty
  3. }
  4. var colors = new MyArray();
  5. colors[0] = "red";
  6. console.log(colors.length); // 1
  7. colors.length = 0;
  8. 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:

  1. class MyArray extends Array {
  2. // empty
  3. }
  4. let items = new MyArray(1, 2, 3, 4),
  5. subitems = items.slice(1, 3);
  6. console.log(items instanceof MyArray); // true
  7. 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:

  1. // several builtin types use species similar to this
  2. class MyClass {
  3. static get [Symbol.species]() {
  4. return this;
  5. }
  6. constructor(value) {
  7. this.value = value;
  8. }
  9. clone() {
  10. return new this.constructor[Symbol.species](this.value);
  11. }
  12. }

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:

  1. class MyClass {
  2. static get [Symbol.species]() {
  3. return this;
  4. }
  5. constructor(value) {
  6. this.value = value;
  7. }
  8. clone() {
  9. return new this.constructor[Symbol.species](this.value);
  10. }
  11. }
  12. class MyDerivedClass1 extends MyClass {
  13. // empty
  14. }
  15. class MyDerivedClass2 extends MyClass {
  16. static get [Symbol.species]() {
  17. return MyClass;
  18. }
  19. }
  20. let instance1 = new MyDerivedClass1("foo"),
  21. clone1 = instance1.clone(),
  22. instance2 = new MyDerivedClass2("bar"),
  23. clone2 = instance2.clone();
  24. console.log(clone1 instanceof MyClass); // true
  25. console.log(clone1 instanceof MyDerivedClass1); // true
  26. console.log(clone2 instanceof MyClass); // true
  27. 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:

  1. class MyArray extends Array {
  2. static get [Symbol.species]() {
  3. return Array;
  4. }
  5. }
  6. let items = new MyArray(1, 2, 3, 4),
  7. subitems = items.slice(1, 3);
  8. console.log(items instanceof MyArray); // true
  9. console.log(subitems instanceof Array); // true
  10. 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.