Please support this book: buy it (PDF, EPUB, MOBI) or donate

15. Classes

15.1 Overview

A class and a subclass:

  1. class Point {
  2. constructor(x, y) {
  3. this.x = x;
  4. this.y = y;
  5. }
  6. toString() {
  7. return `(${this.x}, ${this.y})`;
  8. }
  9. }
  10.  
  11. class ColorPoint extends Point {
  12. constructor(x, y, color) {
  13. super(x, y);
  14. this.color = color;
  15. }
  16. toString() {
  17. return super.toString() + ' in ' + this.color;
  18. }
  19. }

Using the classes:

  1. > const cp = new ColorPoint(25, 8, 'green');
  2.  
  3. > cp.toString();
  4. '(25, 8) in green'
  5.  
  6. > cp instanceof ColorPoint
  7. true
  8. > cp instanceof Point
  9. true

Under the hood, ES6 classes are not something that is radically new: They mainly provide more convenient syntax to create old-school constructor functions. You can see that if you use typeof:

  1. > typeof Point
  2. 'function'

15.2 The essentials

15.2.1 Base classes

A class is defined like this in ECMAScript 6:

  1. class Point {
  2. constructor(x, y) {
  3. this.x = x;
  4. this.y = y;
  5. }
  6. toString() {
  7. return `(${this.x}, ${this.y})`;
  8. }
  9. }

You use this class just like an ES5 constructor function:

  1. > var p = new Point(25, 8);
  2. > p.toString()
  3. '(25, 8)'

In fact, the result of a class definition is a function:

  1. > typeof Point
  2. 'function'

However, you can only invoke a class via new, not via a function call (the rationale behind this is explained later):

  1. > Point()
  2. TypeError: Classes cant be function-called
15.2.1.1 No separators between members of class definitions

There is no separating punctuation between the members of a class definition. For example, the members of an object literal are separated by commas, which are illegal at the top levels of class definitions. Semicolons are allowed, but ignored:

  1. class MyClass {
  2. foo() {}
  3. ; // OK, ignored
  4. , // SyntaxError
  5. bar() {}
  6. }

Semicolons are allowed in preparation for future syntax which may include semicolon-terminated members. Commas are forbidden to emphasize that class definitions are different from object literals.

15.2.1.2 Class declarations are not hoisted

Function declarations are hoisted: When entering a scope, the functions that are declared in it are immediately available – independently of where the declarations happen. That means that you can call a function that is declared later:

  1. foo(); // works, because `foo` is hoisted
  2.  
  3. function foo() {}

In contrast, class declarations are not hoisted. Therefore, a class only exists after execution reached its definition and it was evaluated. Accessing it beforehand leads to a ReferenceError:

  1. new Foo(); // ReferenceError
  2.  
  3. class Foo {}

The reason for this limitation is that classes can have an extends clause whose value is an arbitrary expression. That expression must be evaluated in the proper “location”, its evaluation can’t be hoisted.

Not having hoisting is less limiting than you may think. For example, a function that comes before a class declaration can still refer to that class, but you have to wait until the class declaration has been evaluated before you can call the function.

  1. function functionThatUsesBar() {
  2. new Bar();
  3. }
  4.  
  5. functionThatUsesBar(); // ReferenceError
  6. class Bar {}
  7. functionThatUsesBar(); // OK
15.2.1.3 Class expressions

Similarly to functions, there are two kinds of class definitions, two ways to define a class: class declarations and class expressions.

Similarly to function expressions, class expressions can be anonymous:

  1. const MyClass = class {
  2. ···
  3. };
  4. const inst = new MyClass();

Also similarly to function expressions, class expressions can have names that are only visible inside them:

  1. const MyClass = class Me {
  2. getClassName() {
  3. return Me.name;
  4. }
  5. };
  6. const inst = new MyClass();
  7.  
  8. console.log(inst.getClassName()); // Me
  9. console.log(Me.name); // ReferenceError: Me is not defined

The last two lines demonstrate that Me does not become a variable outside of the class, but can be used inside it.

15.2.2 Inside the body of a class definition

A class body can only contain methods, but not data properties. Prototypes having data properties is generally considered an anti-pattern, so this just enforces a best practice.

15.2.2.1 constructor, static methods, prototype methods

Let’s examine three kinds of methods that you often find in class definitions.

  1. class Foo {
  2. constructor(prop) {
  3. this.prop = prop;
  4. }
  5. static staticMethod() {
  6. return 'classy';
  7. }
  8. prototypeMethod() {
  9. return 'prototypical';
  10. }
  11. }
  12. const foo = new Foo(123);

The object diagram for this class declaration looks as follows. Tip for understanding it: [[Prototype]] is an inheritance relationship between objects, while prototype is a normal property whose value is an object. The property prototype is only special w.r.t. the new operator using its value as the prototype for instances it creates.

15. Classes - 图1

First, the pseudo-method constructor. This method is special, as it defines the function that represents the class:

  1. > Foo === Foo.prototype.constructor
  2. true
  3. > typeof Foo
  4. 'function'

It is sometimes called a class constructor. It has features that normal constructor functions don’t have (mainly the ability to constructor-call its superconstructor via super(), which is explained later).

Second, static methods.Static properties (or class properties) are properties of Foo itself. If you prefix a method definition with static, you create a class method:

  1. > typeof Foo.staticMethod
  2. 'function'
  3. > Foo.staticMethod()
  4. 'classy'

Third, prototype methods. The prototype properties of Foo are the properties of Foo.prototype. They are usually methods and inherited by instances of Foo.

  1. > typeof Foo.prototype.prototypeMethod
  2. 'function'
  3. > foo.prototypeMethod()
  4. 'prototypical'
15.2.2.2 Static data properties

For the sake of finishing ES6 classes in time, they were deliberately designed to be “maximally minimal”. That’s why you can currently only create static methods, getters, and setters, but not static data properties. There is a proposal for adding them to the language. Until that proposal is accepted, there are two work-arounds that you can use.

First, you can manually add a static property:

  1. class Point {
  2. constructor(x, y) {
  3. this.x = x;
  4. this.y = y;
  5. }
  6. }
  7. Point.ZERO = new Point(0, 0);

You could use Object.defineProperty() to create a read-only property, but I like the simplicity of an assignment.

Second, you can create a static getter:

  1. class Point {
  2. constructor(x, y) {
  3. this.x = x;
  4. this.y = y;
  5. }
  6. static get ZERO() {
  7. return new Point(0, 0);
  8. }
  9. }

In both cases, you get a property Point.ZERO that you can read. In the first case, the same instance is returned every time. In the second case, a new instance is returned every time.

15.2.2.3 Getters and setters

The syntax for getters and setters is just like in ECMAScript 5 object literals:

  1. class MyClass {
  2. get prop() {
  3. return 'getter';
  4. }
  5. set prop(value) {
  6. console.log('setter: '+value);
  7. }
  8. }

You use MyClass as follows.

  1. > const inst = new MyClass();
  2. > inst.prop = 123;
  3. setter: 123
  4. > inst.prop
  5. 'getter'
15.2.2.4 Computed method names

You can define the name of a method via an expression, if you put it in square brackets. For example, the following ways of defining Foo are all equivalent.

  1. class Foo {
  2. myMethod() {}
  3. }
  4.  
  5. class Foo {
  6. ['my'+'Method']() {}
  7. }
  8.  
  9. const m = 'myMethod';
  10. class Foo {
  11. [m]() {}
  12. }

Several special methods in ECMAScript 6 have keys that are symbols. Computed method names allow you to define such methods. For example, if an object has a method whose key is Symbol.iterator, it is iterable. That means that its contents can be iterated over by the for-of loop and other language mechanisms.

  1. class IterableClass {
  2. [Symbol.iterator]() {
  3. ···
  4. }
  5. }
15.2.2.5 Generator methods

If you prefix a method definition with an asterisk (*), it becomes a generator method. Among other things, a generator is useful for defining the method whose key is Symbol.iterator. The following code demonstrates how that works.

  1. class IterableArguments {
  2. constructor(...args) {
  3. this.args = args;
  4. }
  5. * [Symbol.iterator]() {
  6. for (const arg of this.args) {
  7. yield arg;
  8. }
  9. }
  10. }
  11.  
  12. for (const x of new IterableArguments('hello', 'world')) {
  13. console.log(x);
  14. }
  15.  
  16. // Output:
  17. // hello
  18. // world

15.2.3 Subclassing

The extends clause lets you create a subclass of an existing constructor (which may or may not have been defined via a class):

  1. class Point {
  2. constructor(x, y) {
  3. this.x = x;
  4. this.y = y;
  5. }
  6. toString() {
  7. return `(${this.x}, ${this.y})`;
  8. }
  9. }
  10.  
  11. class ColorPoint extends Point {
  12. constructor(x, y, color) {
  13. super(x, y); // (A)
  14. this.color = color;
  15. }
  16. toString() {
  17. return super.toString() + ' in ' + this.color; // (B)
  18. }
  19. }

Again, this class is used like you’d expect:

  1. > const cp = new ColorPoint(25, 8, 'green');
  2. > cp.toString()
  3. '(25, 8) in green'
  4.  
  5. > cp instanceof ColorPoint
  6. true
  7. > cp instanceof Point
  8. true

There are two kinds of classes:

  • Point is a base class, because it doesn’t have an extends clause.
  • ColorPoint is a derived class. There are two ways of using super:

  • A class constructor (the pseudo-method constructor in a class definition) uses it like a function call (super(···)), in order to make a superconstructor call (line A).

  • Method definitions (in object literals or classes, with or without static) use it like property references (super.prop) or method calls (super.method(···)), in order to refer to superproperties (line B).
15.2.3.1 The prototype of a subclass is the superclass

The prototype of a subclass is the superclass in ECMAScript 6:

  1. > Object.getPrototypeOf(ColorPoint) === Point
  2. true

That means that static properties are inherited:

  1. class Foo {
  2. static classMethod() {
  3. return 'hello';
  4. }
  5. }
  6.  
  7. class Bar extends Foo {
  8. }
  9. Bar.classMethod(); // 'hello'

You can even super-call static methods:

  1. class Foo {
  2. static classMethod() {
  3. return 'hello';
  4. }
  5. }
  6.  
  7. class Bar extends Foo {
  8. static classMethod() {
  9. return super.classMethod() + ', too';
  10. }
  11. }
  12. Bar.classMethod(); // 'hello, too'
15.2.3.2 Superconstructor calls

In a derived class, you must call super() before you can use this:

  1. class Foo {}
  2.  
  3. class Bar extends Foo {
  4. constructor(num) {
  5. const tmp = num * 2; // OK
  6. this.num = num; // ReferenceError
  7. super();
  8. this.num = num; // OK
  9. }
  10. }

Implicitly leaving a derived constructor without calling super() also causes an error:

  1. class Foo {}
  2.  
  3. class Bar extends Foo {
  4. constructor() {
  5. }
  6. }
  7.  
  8. const bar = new Bar(); // ReferenceError
15.2.3.3 Overriding the result of a constructor

Just like in ES5, you can override the result of a constructor by explicitly returning an object:

  1. class Foo {
  2. constructor() {
  3. return Object.create(null);
  4. }
  5. }
  6. console.log(new Foo() instanceof Foo); // false

If you do so, it doesn’t matter whether this has been initialized or not. In other words: you don’t have to call super() in a derived constructor if you override the result in this manner.

15.2.3.4 Default constructors for classes

If you don’t specify a constructor for a base class, the following definition is used:

  1. constructor() {}

For derived classes, the following default constructor is used:

  1. constructor(...args) {
  2. super(...args);
  3. }
15.2.3.5 Subclassing built-in constructors

In ECMAScript 6, you can finally subclass all built-in constructors (there are work-arounds for ES5, but these have significant limitations).

For example, you can now create your own exception classes (that will inherit the feature of having a stack trace in most engines):

  1. class MyError extends Error {
  2. }
  3. throw new MyError('Something happened!');

You can also create subclasses of Array whose instances properly handle length:

  1. class Stack extends Array {
  2. get top() {
  3. return this[this.length - 1];
  4. }
  5. }
  6.  
  7. var stack = new Stack();
  8. stack.push('world');
  9. stack.push('hello');
  10. console.log(stack.top); // hello
  11. console.log(stack.length); // 2

Note that subclassing Array is usually not the best solution. It’s often better to create your own class (whose interface you control) and to delegate to an Array in a private property.

15.3 Private data for classes

This section explains four approaches for managing private data for ES6 classes:

  • Keeping private data in the environment of a class constructor
  • Marking private properties via a naming convention (e.g. a prefixed underscore)
  • Keeping private data in WeakMaps
  • Using symbols as keys for private properties Approaches #1 and #2 were already common in ES5, for constructors. Approaches #3 and #4 are new in ES6. Let’s implement the same example four times, via each of the approaches.

15.3.1 Private data via constructor environments

Our running example is a class Countdown that invokes a callback action once a counter (whose initial value is counter) reaches zero. The two parameters action and counter should be stored as private data.

In the first implementation, we store action and counter in the environment of the class constructor. An environment is the internal data structure, in which a JavaScript engine stores the parameters and local variables that come into existence whenever a new scope is entered (e.g. via a function call or a constructor call). This is the code:

  1. class Countdown {
  2. constructor(counter, action) {
  3. Object.assign(this, {
  4. dec() {
  5. if (counter < 1) return;
  6. counter--;
  7. if (counter === 0) {
  8. action();
  9. }
  10. }
  11. });
  12. }
  13. }

Using Countdown looks like this:

  1. > const c = new Countdown(2, () => console.log('DONE'));
  2. > c.dec();
  3. > c.dec();
  4. DONE

Pros:

  • The private data is completely safe
  • The names of private properties won’t clash with the names of other private properties (of superclasses or subclasses). Cons:

  • The code becomes less elegant, because you need to add all methods to the instance, inside the constructor (at least those methods that need access to the private data).

  • Due to the instance methods, the code wastes memory. If the methods were prototype methods, they would be shared. More information on this technique: Sect. “Private Data in the Environment of a Constructor (Crockford Privacy Pattern)” in “Speaking JavaScript”.

15.3.2 Private data via a naming convention

The following code keeps private data in properties whose names a marked via a prefixed underscore:

  1. class Countdown {
  2. constructor(counter, action) {
  3. this._counter = counter;
  4. this._action = action;
  5. }
  6. dec() {
  7. if (this._counter < 1) return;
  8. this._counter--;
  9. if (this._counter === 0) {
  10. this._action();
  11. }
  12. }
  13. }

Pros:

  • Code looks nice.
  • We can use prototype methods. Cons:

  • Not safe, only a guideline for client code.

  • The names of private properties can clash.

15.3.3 Private data via WeakMaps

There is a neat technique involving WeakMaps that combines the advantage of the first approach (safety) with the advantage of the second approach (being able to use prototype methods). This technique is demonstrated in the following code: we use the WeakMaps _counter and _action to store private data.

  1. const _counter = new WeakMap();
  2. const _action = new WeakMap();
  3. class Countdown {
  4. constructor(counter, action) {
  5. _counter.set(this, counter);
  6. _action.set(this, action);
  7. }
  8. dec() {
  9. let counter = _counter.get(this);
  10. if (counter < 1) return;
  11. counter--;
  12. _counter.set(this, counter);
  13. if (counter === 0) {
  14. _action.get(this)();
  15. }
  16. }
  17. }

Each of the two WeakMaps _counter and _action maps objects to their private data. Due to how WeakMaps work that won’t prevent objects from being garbage-collected. As long as you keep the WeakMaps hidden from the outside world, the private data is safe.

If you want to be even safer, you can store WeakMap.prototype.get and WeakMap.prototype.set in variables and invoke those (instead of the methods, dynamically):

  1. const set = WeakMap.prototype.set;
  2. ···
  3. set.call(_counter, this, counter);
  4. // _counter.set(this, counter);

Then your code won’t be affected if malicious code replaces those methods with ones that snoop on our private data. However, you are only protected against code that runs after your code. There is nothing you can do if it runs before yours.

Pros:

  • We can use prototype methods.
  • Safer than a naming convention for property keys.
  • The names of private properties can’t clash.
  • Relatively elegant. Con:

  • Code is not as elegant as a naming convention.

15.3.4 Private data via symbols

Another storage location for private data are properties whose keys are symbols:

  1. const _counter = Symbol('counter');
  2. const _action = Symbol('action');
  3.  
  4. class Countdown {
  5. constructor(counter, action) {
  6. this[_counter] = counter;
  7. this[_action] = action;
  8. }
  9. dec() {
  10. if (this[_counter] < 1) return;
  11. this[_counter]--;
  12. if (this[_counter] === 0) {
  13. this[_action]();
  14. }
  15. }
  16. }

Each symbol is unique, which is why a symbol-valued property key will never clash with any other property key. Additionally, symbols are somewhat hidden from the outside world, but not completely:

  1. const c = new Countdown(2, () => console.log('DONE'));
  2.  
  3. console.log(Object.keys(c));
  4. // []
  5. console.log(Reflect.ownKeys(c));
  6. // [ Symbol(counter), Symbol(action) ]

Pros:

  • We can use prototype methods.
  • The names of private properties can’t clash. Cons:

  • Code is not as elegant as a naming convention.

  • Not safe: you can list all property keys (including symbols!) of an object via Reflect.ownKeys().

15.3.5 Further reading

15.4 Simple mixins

Subclassing in JavaScript is used for two reasons:

  • Interface inheritance: Every object that is an instance of a subclass (as tested by instanceof) is also an instance of the superclass. The expectation is that subclass instances behave like superclass instances, but may do more.
  • Implementation inheritance: Superclasses pass on functionality to their subclasses. The usefulness of classes for implementation inheritance is limited, because they only support single inheritance (a class can have at most one superclass). Therefore, it is impossible to inherit tool methods from multiple sources – they must all come from the superclass.

So how can we solve this problem? Let’s explore a solution via an example. Consider a management system for an enterprise where Employee is a subclass of Person.

  1. class Person { ··· }
  2. class Employee extends Person { ··· }

Additionally, there are tool classes for storage and for data validation:

  1. class Storage {
  2. save(database) { ··· }
  3. }
  4. class Validation {
  5. validate(schema) { ··· }
  6. }

It would be nice if we could include the tool classes like this:

  1. // Invented ES6 syntax:
  2. class Employee extends Storage, Validation, Person { ··· }

That is, we want Employee to be a subclass of Storage which should be a subclass of Validation which should be a subclass of Person. Employee and Person will only be used in one such chain of classes. But Storage and Validation will be used multiple times. We want them to be templates for classes whose superclasses we fill in. Such templates are called abstract subclasses or mixins.

One way of implementing a mixin in ES6 is to view it as a function whose input is a superclass and whose output is a subclass extending that superclass:

  1. const Storage = Sup => class extends Sup {
  2. save(database) { ··· }
  3. };
  4. const Validation = Sup => class extends Sup {
  5. validate(schema) { ··· }
  6. };

Here, we profit from the operand of the extends clause not being a fixed identifier, but an arbitrary expression. With these mixins, Employee is created like this:

  1. class Employee extends Storage(Validation(Person)) { ··· }

Acknowledgement. The first occurrence of this technique that I’m aware of is a Gist by Sebastian Markbåge.

15.5 The details of classes

What we have seen so far are the essentials of classes. You only need to read on if you are interested how things happen under the hood. Let’s start with the syntax of classes. The following is a slightly modified version of the syntax shown in Sect. A.4 of the ECMAScript 6 specification.

  1. ClassDeclaration:
  2. "class" BindingIdentifier ClassTail
  3. ClassExpression:
  4. "class" BindingIdentifier? ClassTail
  5.  
  6. ClassTail:
  7. ClassHeritage? "{" ClassBody? "}"
  8. ClassHeritage:
  9. "extends" AssignmentExpression
  10. ClassBody:
  11. ClassElement+
  12. ClassElement:
  13. MethodDefinition
  14. "static" MethodDefinition
  15. ";"
  16.  
  17. MethodDefinition:
  18. PropName "(" FormalParams ")" "{" FuncBody "}"
  19. "*" PropName "(" FormalParams ")" "{" GeneratorBody "}"
  20. "get" PropName "(" ")" "{" FuncBody "}"
  21. "set" PropName "(" PropSetParams ")" "{" FuncBody "}"
  22.  
  23. PropertyName:
  24. LiteralPropertyName
  25. ComputedPropertyName
  26. LiteralPropertyName:
  27. IdentifierName /* foo */
  28. StringLiteral /* "foo" */
  29. NumericLiteral /* 123.45, 0xFF */
  30. ComputedPropertyName:
  31. "[" Expression "]"

Two observations:

  • The value to be extended can be produced by an arbitrary expression. Which means that you’ll be able to write code such as the following:
  1. class Foo extends combine(MyMixin, MySuperClass) {}
  • Semicolons are allowed between methods.

15.5.1 Various checks

  • Error checks: the class name cannot be eval or arguments; duplicate class element names are not allowed; the name constructor can only be used for a normal method, not for a getter, a setter or a generator method.
  • Classes can’t be function-called. They throw a TypeException if they are.
  • Prototype methods cannot be used as constructors:
  1. class C {
  2. m() {}
  3. }
  4. new C.prototype.m(); // TypeError

15.5.2 Attributes of properties

Class declarations create (mutable) let bindings. The following table describes the attributes of properties related to a given class Foo:

writableenumerableconfigurable
Static properties Foo.truefalsetrue
Foo.prototypefalsefalsefalse
Foo.prototype.constructorfalsefalsetrue
Prototype properties Foo.prototype.truefalsetrue

Notes:

  • Many properties are writable, to allow for dynamic patching.
  • A constructor and the object in its property prototype have an immutable bidirectional link.
  • Method definitions in object literals produce enumerable properties.

15.5.3 Classes have inner names

Classes have lexical inner names, just like named function expressions.

15.5.3.1 The inner names of named function expressions

You may know that named function expressions have lexical inner names:

  1. const fac = function me(n) {
  2. if (n > 0) {
  3. // Use inner name `me` to
  4. // refer to function
  5. return n * me(n-1);
  6. } else {
  7. return 1;
  8. }
  9. };
  10. console.log(fac(3)); // 6

The name me of the named function expression becomes a lexically bound variable that is unaffected by which variable currently holds the function.

15.5.3.2 The inner names of classes

Interestingly, ES6 classes also have lexical inner names that you can use in methods (constructor methods and regular methods):

  1. class C {
  2. constructor() {
  3. // Use inner name C to refer to class
  4. console.log(`constructor: ${C.prop}`);
  5. }
  6. logProp() {
  7. // Use inner name C to refer to class
  8. console.log(`logProp: ${C.prop}`);
  9. }
  10. }
  11. C.prop = 'Hi!';
  12.  
  13. const D = C;
  14. C = null;
  15.  
  16. // C is not a class, anymore:
  17. new C().logProp();
  18. // TypeError: C is not a function
  19.  
  20. // But inside the class, the identifier C
  21. // still works
  22. new D().logProp();
  23. // constructor: Hi!
  24. // logProp: Hi!

(In the ES6 spec the inner name is set up by the dynamic semantics of ClassDefinitionEvaluation.)

Acknowledgement: Thanks to Michael Ficarra for pointing out that classes have inner names.

15.6 The details of subclassing

In ECMAScript 6, subclassing looks as follows.

  1. class Person {
  2. constructor(name) {
  3. this.name = name;
  4. }
  5. toString() {
  6. return `Person named ${this.name}`;
  7. }
  8. static logNames(persons) {
  9. for (const person of persons) {
  10. console.log(person.name);
  11. }
  12. }
  13. }
  14.  
  15. class Employee extends Person {
  16. constructor(name, title) {
  17. super(name);
  18. this.title = title;
  19. }
  20. toString() {
  21. return `${super.toString()} (${this.title})`;
  22. }
  23. }
  24.  
  25. const jane = new Employee('Jane', 'CTO');
  26. console.log(jane.toString()); // Person named Jane (CTO)

The next section examines the structure of the objects that were created by the previous example. The section after that examines how jane is allocated and initialized.

15.6.1 Prototype chains

The previous example creates the following objects.

15. Classes - 图2

Prototype chains are objects linked via the [[Prototype]] relationship (which is an inheritance relationship). In the diagram, you can see two prototype chains:

15.6.1.1 Left column: classes (functions)

The prototype of a derived class is the class it extends. The reason for this setup is that you want a subclass to inherit all properties of its superclass:

  1. > Employee.logNames === Person.logNames
  2. true

The prototype of a base class is Function.prototype, which is also the prototype of functions:

  1. > const getProto = Object.getPrototypeOf.bind(Object);
  2.  
  3. > getProto(Person) === Function.prototype
  4. true
  5. > getProto(function () {}) === Function.prototype
  6. true

That means that base classes and all their derived classes (their prototypees) are functions. Traditional ES5 functions are essentially base classes.

15.6.1.2 Right column: the prototype chain of the instance

The main purpose of a class is to set up this prototype chain. The prototype chain ends with Object.prototype (whose prototype is null). That makes Object an implicit superclass of every base class (as far as instances and the instanceof operator are concerned).

The reason for this setup is that you want the instance prototype of a subclass to inherit all properties of the superclass instance prototype.

As an aside, objects created via object literals also have the prototype Object.prototype:

  1. > Object.getPrototypeOf({}) === Object.prototype
  2. true

15.6.2 Allocating and initializing instances

The data flow between class constructors is different from the canonical way of subclassing in ES5. Under the hood, it roughly looks as follows.

  1. // Base class: this is where the instance is allocated
  2. function Person(name) {
  3. // Performed before entering this constructor:
  4. this = Object.create(new.target.prototype);
  5.  
  6. this.name = name;
  7. }
  8. ···
  9.  
  10. function Employee(name, title) {
  11. // Performed before entering this constructor:
  12. this = uninitialized;
  13.  
  14. this = Reflect.construct(Person, [name], new.target); // (A)
  15. // super(name);
  16.  
  17. this.title = title;
  18. }
  19. Object.setPrototypeOf(Employee, Person);
  20. ···
  21.  
  22. const jane = Reflect.construct( // (B)
  23. Employee, ['Jane', 'CTO'],
  24. Employee);
  25. // const jane = new Employee('Jane', 'CTO')

The instance object is created in different locations in ES6 and ES5:

  • In ES6, it is created in the base constructor, the last in a chain of constructor calls. The superconstructor is invoked via super(), which triggers a constructor call.
  • In ES5, it is created in the operand of new, the first in a chain of constructor calls. The superconstructor is invoked via a function call. The previous code uses two new ES6 features:

  • new.target is an implicit parameter that all functions have. In a chain of constructor calls, its role is similar to this in a chain of supermethod calls.

    • If a constructor is directly invoked via new (as in line B), the value of new.target is that constructor.
    • If a constructor is called via super() (as in line A), the value of new.target is the new.target of the constructor that makes the call.
    • During a normal function call, it is undefined. That means that you can use new.target to determine whether a function was function-called or constructor-called (via new).
    • Inside an arrow function, new.target refers to the new.target of the surrounding non-arrow function.
  • Reflect.construct() lets you make constructor calls while specifying new.target via the last parameter. The advantage of this way of subclassing is that it enables normal code to subclass built-in constructors (such as Error and Array). A later section explains why a different approach was necessary.

As a reminder, here is how you do subclassing in ES5:

  1. function Person(name) {
  2. this.name = name;
  3. }
  4. ···
  5.  
  6. function Employee(name, title) {
  7. Person.call(this, name);
  8. this.title = title;
  9. }
  10. Employee.prototype = Object.create(Person.prototype);
  11. Employee.prototype.constructor = Employee;
  12. ···
15.6.2.1 Safety checks
  • this originally being uninitialized in derived constructors means that an error is thrown if they access this in any way before they have called super().
  • Once this is initialized, calling super() produces a ReferenceError. This protects you against calling super() twice.
  • If a constructor returns implicitly (without a return statement), the result is this. If this is uninitialized, a ReferenceError is thrown. This protects you against forgetting to call super().
  • If a constructor explicitly returns a non-object (including undefined and null), the result is this (this behavior is required to remain compatible with ES5 and earlier). If this is uninitialized, a TypeError is thrown.
  • If a constructor explicitly returns an object, it is used as its result. Then it doesn’t matter whether this is initialized or not.
15.6.2.2 The extends clause

Let’s examine how the extends clause influences how a class is set up (Sect. 14.5.14 of the spec).

The value of an extends clause must be “constructible” (invocable via new). null is allowed, though.

  1. class C {
  2. }
  • Constructor kind: base
  • Prototype of C: Function.prototype (like a normal function)
  • Prototype of C.prototype: Object.prototype (which is also the prototype of objects created via object literals)
  1. class C extends B {
  2. }
  • Constructor kind: derived
  • Prototype of C: B
  • Prototype of C.prototype: B.prototype
  1. class C extends Object {
  2. }
  • Constructor kind: derived
  • Prototype of C: Object
  • Prototype of C.prototype: Object.prototype Note the following subtle difference with the first case: If there is no extends clause, the class is a base class and allocates instances. If a class extends Object, it is a derived class and Object allocates the instances. The resulting instances (including their prototype chains) are the same, but you get there differently.
  1. class C extends null {
  2. }
  • Constructor kind: base (as of ES2016)
  • Prototype of C: Function.prototype
  • Prototype of C.prototype: null Such a class lets you avoid Object.prototype in the prototype chain.

15.6.3 Why can’t you subclass built-in constructors in ES5?

In ECMAScript 5, most built-in constructors can’t be subclassed (several work-arounds exist).

To understand why, let’s use the canonical ES5 pattern to subclass Array. As we shall soon find out, this doesn’t work.

  1. function MyArray(len) {
  2. Array.call(this, len); // (A)
  3. }
  4. MyArray.prototype = Object.create(Array.prototype);

Unfortunately, if we instantiate MyArray, we find out that it doesn’t work properly: The instance property length does not change in reaction to us adding Array elements:

  1. > var myArr = new MyArray(0);
  2. > myArr.length
  3. 0
  4. > myArr[0] = 'foo';
  5. > myArr.length
  6. 0

There are two obstracles that prevent myArr from being a proper Array.

First obstacle: initialization. The this you hand to the constructor Array (in line A) is completely ignored. That means you can’t use Array to set up the instance that was created for MyArray.

  1. > var a = [];
  2. > var b = Array.call(a, 3);
  3. > a !== b // a is ignored, b is a new object
  4. true
  5. > b.length // set up correctly
  6. 3
  7. > a.length // unchanged
  8. 0

Second obstacle: allocation. The instance objects created by Array are exotic (a term used by the ECMAScript specification for objects that have features that normal objects don’t have): Their property length tracks and influences the management of Array elements. In general, exotic objects can be created from scratch, but you can’t convert an existing normal object into an exotic one. Unfortunately, that is what Array would have to do, when called in line A: It would have to turn the normal object created for MyArray into an exotic Array object.

15.6.3.1 The solution: ES6 subclassing

In ECMAScript 6, subclassing Array looks as follows:

  1. class MyArray extends Array {
  2. constructor(len) {
  3. super(len);
  4. }
  5. }

This works:

  1. > const myArr = new MyArray(0);
  2. > myArr.length
  3. 0
  4. > myArr[0] = 'foo';
  5. > myArr.length
  6. 1

Let’s examine how the ES6 approach to subclassing removes the previously mentioned obstacles:

  • The first obstacle, Array not being able to set up an instance, is removed by Array returning a fully configured instance. In contrast to ES5, this instance has the prototype of the subclass.
  • The second obstacle, subconstructors not creating exotic instances, is removed by derived classes relying on base classes for allocating instances.

15.6.4 Referring to superproperties in methods

The following ES6 code makes a supermethod call in line B.

  1. class Person {
  2. constructor(name) {
  3. this.name = name;
  4. }
  5. toString() { // (A)
  6. return `Person named ${this.name}`;
  7. }
  8. }
  9.  
  10. class Employee extends Person {
  11. constructor(name, title) {
  12. super(name);
  13. this.title = title;
  14. }
  15. toString() {
  16. return `${super.toString()} (${this.title})`; // (B)
  17. }
  18. }
  19.  
  20. const jane = new Employee('Jane', 'CTO');
  21. console.log(jane.toString()); // Person named Jane (CTO)

To understand how super-calls work, let’s look at the object diagram of jane:

15. Classes - 图3

In line B, Employee.prototype.toString makes a super-call (line B) to the method (starting in line A) that it has overridden. Let’s call the object, in which a method is stored, the home object of that method. For example, Employee.prototype is the home object of Employee.prototype.toString().

The super-call in line B involves three steps:

  • Start your search in the prototype of the home object of the current method.
  • Look for a method whose name is toString. That method may be found in the object where the search started or later in the prototype chain.
  • Call that method with the current this. The reason for doing so is: the super-called method must be able to access the same instance properties (in our example, the own properties of jane). Note that even if you are only getting (super.prop) or setting (super.prop = 123) a superproperty (versus making a method call), this may still (internally) play a role in step #3, because a getter or a setter may be invoked.

Let’s express these steps in three different – but equivalent – ways:

  1. // Variation 1: supermethod calls in ES5
  2. var result = Person.prototype.toString.call(this) // steps 1,2,3
  3.  
  4. // Variation 2: ES5, refactored
  5. var superObject = Person.prototype; // step 1
  6. var superMethod = superObject.toString; // step 2
  7. var result = superMethod.call(this) // step 3
  8.  
  9. // Variation 3: ES6
  10. var homeObject = Employee.prototype;
  11. var superObject = Object.getPrototypeOf(homeObject); // step 1
  12. var superMethod = superObject.toString; // step 2
  13. var result = superMethod.call(this) // step 3

Variation 3 is how ECMAScript 6 handles super-calls. This approach is supported by two internal bindings that the environments of functions have (environments provide storage space, so-called bindings, for the variables in a scope):

  • [[thisValue]]: This internal binding also exists in ECMAScript 5 and stores the value of this.
  • [[HomeObject]]: Refers to the home object of the environment’s function. Filled in via the internal slot [[HomeObject]] that all methods have that use super. Both the binding and the slot are new in ECMAScript 6.

Methods are a special kind of function now

In a class, a method definition that uses super creates a special kind of function: It is still a function, but it has the internal slot [[HomeObject]]. That slot is set up by the method definition and can’t be changed in JavaScript. Therefore, you can’t meaningfully move such a method to a different object. (But maybe it’ll be possible in a future version of ECMAScript.)

15.6.4.1 Where can you use super?

Referring to superproperties is handy whenever prototype chains are involved, which is why you can use it in method definitions (incl. generator method definitions, getters and setters) inside object literals and class definitions. The class can be derived or not, the method can be static or not.

Using super to refer to a property is not allowed in function declarations, function expressions and generator functions.

15.6.4.2 Pitfall: A method that uses super can’t be moved

You can’t move a method that uses super: Such a method has the internal slot [[HomeObject]] that ties it to the object it was created in. If you move it via an assignment, it will continue to refer to the superproperties of the original object. In future ECMAScript versions, there may be a way to transfer such a method, too.

15.7 The species pattern

One more mechanism of built-in constructors has been made extensible in ECMAScript 6: Sometimes a method creates new instances of its class. If you create a subclass – should the method return an instance of its class or an instance of the subclass? A few built-in ES6 methods let you configure how they create instances via the so-called species pattern.

As an example, consider a subclass SortedArray of Array. If we invoke map() on instances of that class, we want it to return instances of Array, to avoid unnecessary sorting. By default, map() returns instances of the receiver (this), but the species patterns lets you change that.

15.7.1 Helper methods for examples

In the following three sections, I’ll use two helper functions in the examples:

  1. function isObject(value) {
  2. return (value !== null
  3. && (typeof value === 'object'
  4. || typeof value === 'function'));
  5. }
  6.  
  7. /**
  8. * Spec-internal operation that determines whether `x`
  9. * can be used as a constructor.
  10. */
  11. function isConstructor(x) {
  12. ···
  13. }

15.7.2 The standard species pattern

The standard species pattern is used by Promise.prototype.then(), the filter() method of Typed Arrays and other operations. It works as follows:

  • If this.constructor[Symbol.species] exists, use it as a constructor for the new instance.
  • Otherwise, use a default constructor (e.g. Array for Arrays). Implemented in JavaScript, the pattern would look like this:
  1. function SpeciesConstructor(O, defaultConstructor) {
  2. const C = O.constructor;
  3. if (C === undefined) {
  4. return defaultConstructor;
  5. }
  6. if (! isObject(C)) {
  7. throw new TypeError();
  8. }
  9. const S = C[Symbol.species];
  10. if (S === undefined || S === null) {
  11. return defaultConstructor;
  12. }
  13. if (! isConstructor(S)) {
  14. throw new TypeError();
  15. }
  16. return S;
  17. }

15.7.3 The species pattern for Arrays

Normal Arrays implement the species pattern slightly differently:

  1. function ArraySpeciesCreate(self, length) {
  2. let C = undefined;
  3. // If the receiver `self` is an Array,
  4. // we use the species pattern
  5. if (Array.isArray(self)) {
  6. C = self.constructor;
  7. if (isObject(C)) {
  8. C = C[Symbol.species];
  9. }
  10. }
  11. // Either `self` is not an Array or the species
  12. // pattern didn’t work out:
  13. // create and return an Array
  14. if (C === undefined || C === null) {
  15. return new Array(length);
  16. }
  17. if (! IsConstructor(C)) {
  18. throw new TypeError();
  19. }
  20. return new C(length);
  21. }

Array.prototype.map() creates the Array it returns via ArraySpeciesCreate(this, this.length).

15.7.4 The species pattern in static methods

Promises use a variant of the species pattern for static methods such as Promise.all():

  1. let C = this; // default
  2. if (! isObject(C)) {
  3. throw new TypeError();
  4. }
  5. // The default can be overridden via the property `C[Symbol.species]`
  6. const S = C[Symbol.species];
  7. if (S !== undefined && S !== null) {
  8. C = S;
  9. }
  10. if (!IsConstructor(C)) {
  11. throw new TypeError();
  12. }
  13. const instance = new C(···);

15.7.5 Overriding the default species in subclasses

This is the default getter for the property [Symbol.species]:

  1. static get [Symbol.species]() {
  2. return this;
  3. }

This default getter is implemented by the built-in classes Array, ArrayBuffer, Map, Promise, RegExp, Set and %TypedArray%. It is automatically inherited by subclasses of these built-in classes.

There are two ways in which you can override the default species: with a constructor of your choosing or with null.

15.7.5.1 Setting the species to a constructor of your choosing

You can override the default species via a static getter (line A):

  1. class MyArray1 extends Array {
  2. static get [Symbol.species]() { // (A)
  3. return Array;
  4. }
  5. }

As a result, map() returns an instance of Array:

  1. const result1 = new MyArray1().map(x => x);
  2. console.log(result1 instanceof Array); // true

If you don’t override the default species, map() returns an instance of the subclass:

  1. class MyArray2 extends Array { }
  2.  
  3. const result2 = new MyArray2().map(x => x);
  4. console.log(result2 instanceof MyArray2); // true
15.7.5.1.1 Specifying the species via a data property

If you don’t want to use a static getter, you need to use Object.defineProperty(). You can’t use assignment, as there is already a property with that key that only has a getter. That means that it is read-only and can’t be assigned to.

For example, here we set the species of MyArray1 to Array:

  1. Object.defineProperty(
  2. MyArray1, Symbol.species, {
  3. value: Array
  4. });
15.7.5.2 Setting the species to null

If you set the species to null then the default constructor is used (which one that is depends on which variant of the species pattern is used, consult the previous sections for more information).

  1. class MyArray3 extends Array {
  2. static get [Symbol.species]() {
  3. return null;
  4. }
  5. }
  6.  
  7. const result3 = new MyArray3().map(x => x);
  8. console.log(result3 instanceof Array); // true

15.8 The pros and cons of classes

Classes are controversial within the JavaScript community: On one hand, people coming from class-based languages are happy that they don’t have to deal with JavaScript’s unconventional inheritance mechanisms, anymore. On the other hand, there are many JavaScript programmers who argue that what’s complicated about JavaScript is not prototypal inheritance, but constructors.

ES6 classes provide a few clear benefits:

  • They are backward-compatible with much of the current code.
  • Compared to constructors and constructor inheritance, classes make it easier for beginners to get started.
  • Subclassing is supported within the language.
  • Built-in constructors are subclassable.
  • No library for inheritance is needed, anymore; code will become more portable between frameworks.
  • They provide a foundation for advanced features in the future: traits (or mixins), immutable instances, etc.
  • They help tools that statically analyze code (IDEs, type checkers, style checkers, etc.). Let’s look at a few common complaints about ES6 classes. You will see me agree with most of them, but I also think that they benefits of classes much outweigh their disadvantages. I’m glad that they are in ES6 and I recommend to use them.

15.8.1 Complaint: ES6 classes obscure the true nature of JavaScript inheritance

Yes, ES6 classes do obscure the true nature of JavaScript inheritance. There is an unfortunate disconnect between what a class looks like (its syntax) and how it behaves (its semantics): It looks like an object, but it is a function. My preference would have been for classes to be constructor objects, not constructor functions. I explore that approach in the Proto.js project, via a tiny library (which proves how good a fit this approach is).

However, backward-compatibility matters, which is why classes being constructor functions also makes sense. That way, ES6 code and ES5 are more interoperable.

The disconnect between syntax and semantics will cause some friction in ES6 and later. But you can lead a comfortable life by simply taking ES6 classes at face value. I don’t think the illusion will ever bite you. Newcomers can get started more quickly and later read up on what goes on behind the scenes (after they are more comfortable with the language).

15.8.2 Complaint: Classes provide only single inheritance

Classes only give you single inheritance, which severely limits your freedom of expression w.r.t. object-oriented design. However, the plan has always been for them to be the foundation of a multiple-inheritance mechanism such as traits.

Then a class becomes an instantiable entity and a location where you assemble traits. Until that happens, you will need to resort to libraries if you want multiple inheritance.

15.8.3 Complaint: Classes lock you in, due to mandatory new

If you want to instantiate a class, you are forced to use new in ES6. That means that you can’t switch from a class to a factory function without changing the call sites. That is indeed a limitation, but there are two mitigating factors:

  • You can override the default result returned by the new operator, by returning an object from the constructor method of a class.
  • Due to its built-in modules and classes, ES6 makes it easier for IDEs to refactor code. Therefore, going from new to a function call will be simple. Obviously that doesn’t help you if you don’t control the code that calls your code, as is the case for libraries. Therefore, classes do somewhat limit you syntactically, but, once JavaScript has traits, they won’t limit you conceptually (w.r.t. object-oriented design).

15.9 FAQ: classes

15.9.1 Why can’t classes be function-called?

Function-calling classes is currently forbidden. That was done to keep options open for the future, to eventually add a way to handle function calls via classes.

15.9.2 How do I instantiate a class, given an Array of arguments?

What is the analog of Function.prototype.apply() for classes? That is, if I have a class TheClass and an Array args of arguments, how do I instantiate TheClass?

One way of doing so is via the spread operator ():

  1. function instantiate(TheClass, args) {
  2. return new TheClass(...args);
  3. }

Another option is to use Reflect.construct():

  1. function instantiate(TheClass, args) {
  2. return Reflect.construct(TheClass, args);
  3. }

15.10 What is next for classes?

The design motto for classes was “maximally minimal”. Several advanced features were discussed, but ultimately discarded in order to get a design that would be unanimously accepted by TC39.

Upcoming versions of ECMAScript can now extend this minimal design – classes will provide a foundation for features such as traits (or mixins), value objects (where different objects are equal if they have the same content) and const classes (that produce immutable instances).

15.11 Further reading

The following document is an important source of this chapter: