[N] Classes in ECMAScript 6

REF: http://2ality.com/2015/02/es6-classes-final.html

Recently, TC39 decided on the final semantics of classes in ECMAScript 6 [1]. This blog post explains how their final incarnation works. The most significant recent changes were related to how subclassing is handled.

Overview

class Point {
    constructor(x, y) {
        this.x = x;
        this.y = y;
    }
    toString() {
        return '(' + this.x + ', ' + this.y + ')';
    }
}

class ColorPoint extends Point {
    constructor(x, y, color) {
        super(x, y);
        this.color = color;
    }
    toString() {
        return super.toString() + ' in ' + this.color;
    }
}

let cp = new ColorPoint(25, 8, 'green');
cp.toString(); // '(25, 8) in green'

console.log(cp instanceof ColorPoint); // true
console.log(cp instanceof Point); // true

The essentials

Base classes

A class is defined like this in ECMAScript 6 (ES6):

class Point {
    constructor(x, y) {
        this.x = x;
        this.y = y;
    }
    toString() {
        return '(' + this.x + ', ' + this.y + ')';
    }
}

You use this class just like an ES5 constructor function:

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

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

> typeof Point
'function'

However, you can only invoke a class via new, not via a function call (Sect. 9.2.2 in the spec):

> Point()
TypeError: Classes can’t be function-called

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:

foo(); // works, because `foo` is hoisted

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:

new Foo(); // ReferenceError

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.

function functionThatUsesBar() {
    new Bar();
}

functionThatUsesBar(); // ReferenceError
class Bar {}
functionThatUsesBar(); // OK

Class expressions

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

Also similarly to functions, the identifier of a class expression is only visible within the expression:

const MyClass = class Me {
    getClassName() {
        return Me.name;
    }
};
let inst = new MyClass();
console.log(inst.getClassName()); // Me
console.log(Me.name); // ReferenceError: Me is not defined

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.

constructor, static methods, prototype methods

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

class Foo {
    constructor(prop) {
        this.prop = prop;
    }
    static staticMethod() {
        return 'classy';
    }
    prototypeMethod() {
        return 'prototypical';
    }
}
let 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 because the new operator uses its value as the prototype for instances it creates.

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

> Foo === Foo.prototype.constructor
true
> typeof Foo
'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 super-constructor 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:

> typeof Foo.staticMethod
'function'
> Foo.staticMethod()
'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.

> typeof Foo.prototype.prototypeMethod
'function'
> foo.prototypeMethod()
'prototypical'

Getters and setters

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

class MyClass {
    get prop() {
        return 'getter';
    }
    set prop(value) {
        console.log('setter: '+value);
    }
}

You use MyClass as follows.

> let inst = new MyClass();
> inst.prop = 123;
setter: 123
> inst.prop
'getter'

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.

class Foo {
    myMethod() {}
}

class Foo {
    ['my'+'Method']() {}
}

const m = 'myMethod';
class Foo {
    [m]() {}
}

Several special methods in ECMAScript 6 have keys that are symbols [2]. 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 [3]. That means that its contents can be iterated over by the for-of loop and other language mechanisms.

class IterableClass {
    [Symbol.iterator]() {
        ···
    }
}

Generator methods

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

class IterableArguments {
    constructor(...args) {
        this.args = args;
    }
    * [Symbol.iterator]() {
        for (let arg of this.args) {
            yield arg;
        }
    }
}

for (let x of new IterableArguments('hello', 'world')) {
    console.log(x);
}

// Output:
// hello
// world

Subclassing

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

class Point {
    constructor(x, y) {
        this.x = x;
        this.y = y;
    }
    toString() {
        return '(' + this.x + ', ' + this.y + ')';
    }
}

class ColorPoint extends Point {
    constructor(x, y, color) {
        super(x, y); // (A)
        this.color = color;
    }
    toString() {
        return super.toString() + ' in ' + this.color; // (B)
    }
}

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

> let cp = new ColorPoint(25, 8, 'green');
> cp.toString()
'(25, 8) in green'

> cp instanceof ColorPoint
true
> cp instanceof Point
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 literal) uses it like a function call (super(···)), in order to make a super-constructor 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 super-properties (line B).

The prototype of a subclass is the superclass

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

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

That means that static properties are inherited:

class Foo {
    static classMethod() {
        return 'hello';
    }
}

class Bar extends Foo {
}
Bar.classMethod(); // 'hello'

You can even super-call static methods:

class Foo {
    static classMethod() {
        return 'hello';
    }
}

class Bar extends Foo {
    static classMethod() {
        return super.classMethod() + ', too';
    }
}
Bar.classMethod(); // 'hello, too'

Super-constructor calls

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

class Foo {}

class Bar extends Foo {
    constructor(num) {
        let tmp = num * 2; // OK
        this.num = num; // ReferenceError
        super();
        this.num = num; // OK
    }
}

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

class Foo {}

class Bar extends Foo {
    constructor() {
    }
}

let bar = new Bar(); // ReferenceError

Overriding the result of a constructor

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

class Foo {
    constructor() {
        return Object.create(null);
    }
}
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.

Default constructors for classes

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

constructor() {}

For derived classes, the following default constructor is used:

constructor(...args) {
    super(...args);
}

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):

class MyError extends Error {    
}
throw new MyError('Something happened!');

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

class MyArray extends Array {
    constructor(len) {
        super(len);
    }
}

// Instances of of `MyArray` work like real arrays:
let myArr = new MyArray(0);
console.log(myArr.length); // 0
myArr[0] = 'foo';
console.log(myArr.length); // 1

Note that subclassing built-in constructors is something that engines have to support natively, you won’t get this feature via transpilers.

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.

ClassDeclaration:
    "class" BindingIdentifier ClassTail
ClassExpression:
    "class" BindingIdentifier? ClassTail

ClassTail:
    ClassHeritage? "{" ClassBody? "}"
ClassHeritage:
    "extends" AssignmentExpression
ClassBody:
    ClassElement+
ClassElement:
    MethodDefinition
    "static" MethodDefinition
    ";"

MethodDefinition:
    PropName "(" FormalParams ")" "{" FuncBody "}"
    "*" PropName "(" FormalParams ")" "{" GeneratorBody "}"
    "get" PropName "(" ")" "{" FuncBody "}"
    "set" PropName "(" PropSetParams ")" "{" FuncBody "}"

PropertyName:
    LiteralPropertyName
    ComputedPropertyName
LiteralPropertyName:
    IdentifierName  /* foo */
    StringLiteral   /* "foo" */
    NumericLiteral  /* 123.45, 0xFF */
ComputedPropertyName:
    "[" 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:
    class Foo extends combine(MyMixin, MySuperClass) {}
    
  • Semicolons are allowed between methods.

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:
    class C {
        m() {}
    }
    new C.prototype.m(); // TypeError
    

Attributes of properties

Class declarations create (mutable) let bindings. For a given class Foo:

  • Static methods Foo.* are writable and configurable, but not enumerable. Making them writable allows for dynamic patching.
  • A constructor and the object in its property prototype have an immutable link:
    • Foo.prototype is non-writeable, non-enumerable, non-configurable.
    • Foo.prototype.constructor is non-writeable, non-enumerable, non-configurable.
  • Prototype methods Foo.prototype.* are writable and configurable, but not enumerable.

Note that method definitions in object literals produce enumerable properties.

The details of subclassing

In ECMAScript 6, subclassing looks as follows.

class Point {
    constructor(x, y) {
        this.x = x;
        this.y = y;
    }
    ···
}

class ColorPoint extends Point {
    constructor(x, y, color) {
        super(x, y);
        this.color = color;
    }
    ···
}

let cp = new ColorPoint(25, 8, 'green');

This code produces the following objects.

The next subsection examines the prototype chains (in the two columns), the subsection after that examines how cp is allocated and initialized.

Prototype chains

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

  • Left column: classes (functions). The prototype of a derived class is the class it extends. The prototype of a base class is Function.prototype, which is also the prototype of functions:
    > const getProto = Object.getPrototypeOf.bind(Object);
    
    > getProto(Point) === Function.prototype
    true
    > getProto(function () {}) === Function.prototype
    true
    
  • Right column: the prototype chain of the instance. The whole purpose of a class is to set up this prototype chain. The prototype chain ends with Object.prototype (whose prototype is null), which is also the prototype of objects created via object literals:
    > const getProto = Object.getPrototypeOf.bind(Object);
    
    > getProto(Point.prototype) === Object.prototype
    true
    > getProto({}) === Object.prototype
    true
    

The prototype chain in the left column leads to static properties being inherited.

Allocating and initializing the instance object

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

// Instance is allocated here
function Point(x, y) {
    // Performed before entering this constructor:
    this = Object.create(new.target.prototype);

    this.x = x;
    this.y = y;
}
···

function ColorPoint(x, y, color) {
    // Performed before entering this constructor:
    this = uninitialized;

    this = Reflect.construct(Point, [x, y], new.target); // (A)
        // super(x, y);

    this.color = color;
}
Object.setPrototypeOf(ColorPoint, Point);
···

let cp = Reflect.construct( // (B)
             ColorPoint, [25, 8, 'green'],
             ColorPoint);
    // let cp = new ColorPoint(25, 8, 'green');

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.
  • In ES5, it is created in the operand of new, the first in a chain of constructor calls.

The previous code uses two new ES6 features:

  • new.target is an implicit parameter that all functions have. It is to constructor calls what this is to method calls.
    • If a constructor has been directly invoked via new, its value is that constructor (line B).
    • If a constructor was called via super(), its value is the new.targetof the constructor that made the call (line A).
    • 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() [4] lets you do a constructor call 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.

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.

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.

class C {
}
  • 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)
class C extends B {
}
  • Constructor kind: derived
  • Prototype of C: B
  • Prototype of C.prototype: B.prototype
class C extends Object {
}
  • 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 extendsclause, 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.

class C extends null {
}
  • Constructor kind: derived
  • Prototype of C: Function.prototype
  • Prototype of C.prototype: null

Such a class is not very useful: new-calling it leads to an error, because the default constructor makes a super-constructor call and Function.prototype(the super-constructor) can’t be constructor-called. The only way to make the error go away is by adding a constructor that returns an object.

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.

function MyArray(len) {
    Array.call(this, len); // (A)
}
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:

> var myArr = new MyArray(0);
> myArr.length
0
> myArr[0] = 'foo';
> myArr.length
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.

> var a = [];
> var b = Array.call(a, 3);
> a !== b  // a is ignored, b is a new object
true
> b.length // set up correctly
3
> a.length // unchanged
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.

The solution: ES6 subclassing

In ECMAScript 6, subclassing Array looks as follows:

class MyArray extends Array {
    constructor(len) {
        super(len);
    }
}

This works (but it’s not something that ES6 transpilers can support, it depends on whether a JavaScript engine supports it natively):

> let myArr = new MyArray(0);
> myArr.length
0
> myArr[0] = 'foo';
> myArr.length
1

We can now see how the ES6 approach to subclassing circumvents the obstacles:

  • Allocation happens in the base constructor, which means that Array can allocate an exotic object. While most of the new approach is due to how derived constructors behave, this step requires that a base constructor is aware of new.target and makes new.target.prototype the protoype of the allocated instance.
  • Initialization also happens in the base constructor, a derived constructor receives an initialized object and works with that one instead of passing its own instance to the super-constructor and requiring it to set it up.

Referring to super-properties in methods

The following ES6 code makes a super-method call in line B.

class Point {
    constructor(x, y) {
        this.x = x;
        this.y = y;
    }
    toString() { // (A)
        return '(' + this.x + ', ' + this.y + ')';
    }
}

class ColorPoint extends Point {
    constructor(x, y, color) {
        super(x, y);
        this.color = color;
    }
    toString() {
        return super.toString() // (B)
               + ' in ' + this.color;
    }
}

let cp = new ColorPoint(25, 8, 'green');
console.log(cp.toString()); // (25, 8) in green

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

ColorPoint.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, ColorPoint.prototype is the home object of ColorPoint.prototype.toString().

The super-call in line B involves three steps:

  1. Start your search in the prototype of the home object of the current method.
  2. 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.
  3. 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 properties of cp).

Note that even if you are only getting or setting a property (not calling a method), you still have to consider this in step #3, because the property may be implemented via a getter or a setter.

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

// Variation 1: super-method calls in ES5
var result = Point.prototype.toString.call(this) // steps 1,2,3

// Variation 2: ES5, refactored
var superObject = Point.prototype; // step 1
var superMethod = superObject.toString; // step 2
var result = superMethod.call(this) // step 3

// Variation 3: ES6
var homeObject = ColorPoint.prototype;
var superObject = Object.getPrototypeOf(homeObject); // step 1
var superMethod = superObject.toString; // step 2
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 an internal property [[HomeObject]] that all methods have that use super. Both the binding and the property are new in ECMAScript 6.

A method definition in a class literal that uses super is now special: Its value is still a function, but it has the internal property [[HomeObject]]. That property 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.

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

Referring to super-properties is handy whenever prototype chains are involved, which is why you can use it in method definitions inside object literals and class literals (the class can be derived or not, the method can be static or not).

Constructor calls explained via JavaScript code

The JavaScript code in this section is a much simplified version of how the specification describes constructor calls and super-constructor calls. It may be interesting to you if you prefer code to explanations in human language. Before we can delve into the actual functionality, we need to understand a few other mechanisms.

Internal variables and properties

The specification writes internal variables and properties in double brackets ([[Foo]]). In the code, I use double underscores, instead (__Foo__).

Internal variables used in the code:

  • [[NewTarget]]: The operand of the new operator that triggered the current constructor call (passed on if [[Construct]] is called recursively via super()).
  • [[thisValue]]: Stores the value of this.
  • [[FunctionObject]]: Refers to the function that is currently executed.

Internal properties used in the code:

  • [[Construct]]: All constructor functions (including those created by classes) have this own (non-inherited) method. It implements constructor calls and is invoked by new.
  • [[ConstructorKind]]: A property of constructor functions whose value is either 'base' or 'derived'.

Environments

Environments provide storage space for variables, there is one environment per scope. Environments are managed as a stack. The environment on top of that stack is considered active. The following code is a sketch of how environments are handled.

/**
 * Function environments are special, they have a few more
 * internal variables than other environments.
 * (`Environment` is not shown here)
 */
class FunctionEnvironment extends Environment {
    constructor(Func) {
        // [[FunctionObject]] is a function-specific
        // internal variable
        this.__FunctionObject__ = Func;
    }    
}

/**
 * Push an environment onto the stack
 */
function PushEnvironment(env) { ··· }

/**
 * Pop the topmost environment from the stack
 */
function PopEnvironment() { ··· }

/**
 * Find topmost function environment on stack
 */
function GetThisEnvironment() { ··· }

Constructor calls

Let’s start with the default way (ES6 spec Sect. 9.2.3) in which constructor calls are handled for functions:

/**
 * All constructible functions have this own method,
 * it is called by the `new` operator
 */
AnyFunction.__Construct__ = function (args, newTarget) {
    let Constr = this;
    let kind = Constr.__ConstructorKind__;

    let env = new FunctionEnvironment(Constr);
    env.__NewTarget__ = newTarget;
    if (kind === 'base') {
        env.__thisValue__ = Object.create(newTarget.prototype);
    } else {
        // While `this` is uninitialized, getting or setting it
        // throws a `ReferenceError`
        env.__thisValue__ = uninitialized;
    }

    PushEnvironment(env);
    let result = Constr(...args);
    PopEnvironment();

    // Let’s pretend there is a way to tell whether `result`
    // was explicitly returned or not
    if (WasExplicitlyReturned(result)) {
        if (isObject(result)) {
            return result;
        }
        // Explicit return of a primitive
        if (kind === 'base') {
            // Base constructors must be backwards compatible
            return env.__thisValue__; // always initialized!
        }
        throw new TypeError();
    }
    // Implicit return
    if (env.__thisValue__ === uninitialized) {
        throw new ReferenceError();
    }
    return env.__thisValue__;
}

Super-constructor calls

Super-constructor calls are handled as follows (ES6 spec Sect. 12.3.5.1).

/**
 * Handle super-constructor calls
 */
function super(...args) {
    let env = GetThisEnvironment();
    let newTarget = env.__NewTarget__;
    let activeFunc = env.__FunctionObject__;
    let superConstructor = Object.getPrototypeOf(activeFunc);

    env.__thisValue__ = superConstructor
                        .__Construct__(args, newTarget);
}

The species pattern

One more mechanism of built-in constructors has been made extensible in ECMAScript 6: If a method such as Array.prototype.map() returns a fresh instance, what constructor should it use to create that instance? The default is to use the same constructor that created this, but some subclasses may want it to remain a direct instance of Array. ES6 lets subclasses override the default, via the so-called species pattern:

  • When creating a new instance of Array, methods such as map() use the constructor stored in this.constructor[Symbol.species].
  • If a sub-constructor of Array does nothing, it inherits Array[Symbol.species]. That property is a getter that returns this.

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

class MyArray1 extends Array {
}
let result1 = new MyArray1().map(x => x);
console.log(result1 instanceof MyArray1); // true

class MyArray2 extends Array {
    static get [Symbol.species]() { // (A)
        return Array;
    }
}
let result2 = new MyArray2().map(x => x);
console.log(result2 instanceof MyArray2); // false

An alternative is to use Object.defineProperty() (you can’t use assignment, as that would trigger a setter, which doesn’t exist):

Object.defineProperty(
    MyArray2, Symbol.species, {
        value: Array
    });

The following getters all return this, which means that methods such as Array.prototype.map() use the constructor that created the current instance for their results.

  • Array.get [Symbol.species]()
  • ArrayBuffer.get [Symbol.species]()
  • Map.get [Symbol.species]()
  • Promise.get [Symbol.species]()
  • RegExp.get [Symbol.species]()
  • Set.get [Symbol.species]()
  • %TypedArray%.get [Symbol.species]()

Conclusion

The specialization of functions

There is an interesting trend in ECMAScript 6: Previously, a single kind of function took on three roles: real function, method and constructor. In ES6, there is specialization:

  • Arrow functions are specialized for non-method callbacks, where them picking up the this of their surrounding method or constructor is an advantage. Without this, they don’t make much sense as methods and they throw an exception when invoked via new.
  • Method definitions enable the use of super, by setting up the property [[HomeObject]]. The functions they produce can’t be constructor-called.
  • Class definitions are the only way to create derived constructors (enabling ES6-style subclassing that works for built-in constructors). Class definitions produce functions that can only be constructor-called.

The future of classes

The design maxim 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).

Does JavaScript need 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 unorthodox 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 [5].

ES6 classes provide a few clear benefits:

  • They are backwards 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 (mixins and more).
  • They help tools that statically analyze code (IDEs, type checkers, style checkers, etc.).

I have made my peace with classes and am glad that they are in ES6. I would have preferred them to be prototypal (based on constructor objects [5:1], not constructor functions), but I also understand that backwards compatibility is important.

Further reading

Acknowledgement: #1 was an important source of this blog post.


  1. Exploring ES6: Upgrade to the next version of JavaScript”, book by Axel ↩︎
  2. Symbols in ECMAScript 6 ↩︎
  3. Iterators and generators in ECMAScript 6 ↩︎ ↩︎
  4. Meta programming with ECMAScript 6 proxies ↩︎
  5. Prototypes as classes – an introduction to JavaScript inheritance ↩︎ ↩︎
Advertisements

[R] ECMAScript 6 modules

REF: http://2ality.com/2014/09/es6-modules-final.html

At the end of July 2014, TC39 [1] had another meeting, during which the last details of the ECMAScript 6 (ES6) module syntax were finalized. This blog post gives an overview of the complete ES6 module system.

Module systems for current JavaScript

JavaScript does not have built-in support for modules, but the community has created impressive work-arounds. The two most important (and unfortunately incompatible) standards are:

  • CommonJS Modules: The dominant implementation of this standard is in Node.js (Node.js modules have a few features that go beyond CommonJS). Characteristics:
    • Compact syntax
    • Designed for synchronous loading
    • Main use: server
  • Asynchronous Module Definition (AMD): The most popular implementation of this standard is RequireJS. Characteristics:
    • Slightly more complicated syntax, enabling AMD to work without eval() (or a compilation step).
    • Designed for asynchronous loading
    • Main use: browsers

The above is but a grossly simplified explanation of the current state of affairs. If you want more in-depth material, take a look at “Writing Modular JavaScript With AMD, CommonJS & ES Harmony” by Addy Osmani.

ECMAScript 6 modules

The goal for ECMAScript 6 modules was to create a format that both users of CommonJS and of AMD are happy with:

  • Similar to CommonJS, they have a compact syntax, a preference for single exports and support for cyclic dependencies.
  • Similar to AMD, they have direct support for asynchronous loading and configurable module loading.

Being built into the language allows ES6 modules to go beyond CommonJS and AMD (details are explained later):

  • Their syntax is even more compact than CommonJS’s.
  • Their structure can be statically analyzed (for static checking, optimization, etc.).
  • Their support for cyclic dependencies is better than CommonJS’s.

The ES6 module standard has two parts:

  • Declarative syntax (for importing and exporting)
  • Programmatic loader API: to configure how modules are loaded and to conditionally load modules

An overview of the ES6 module syntax

There are two kinds of exports: named exports (several per module) and default exports (one per module).

Named exports (several per module)

A module can export multiple things by prefixing their declarations with the keyword export. These exports are distinguished by their names and are called named exports.

//------ lib.js ------
export const sqrt = Math.sqrt;
export function square(x) {
    return x * x;
}
export function diag(x, y) {
    return sqrt(square(x) + square(y));
}

//------ main.js ------
import { square, diag } from 'lib';
console.log(square(11)); // 121
console.log(diag(4, 3)); // 5

There are other ways to specify named exports (which are explained later), but I find this one quite convenient: simply write your code as if there were no outside world, then label everything that you want to export with a keyword.

If you want to, you can also import the whole module and refer to its named exports via property notation:

//------ main.js ------
import * as lib from 'lib';
console.log(lib.square(11)); // 121
console.log(lib.diag(4, 3)); // 5

The same code in CommonJS syntax: For a while, I tried several clever strategies to be less redundant with my module exports in Node.js. Now I prefer the following simple but slightly verbose style that is reminiscent of the revealing module pattern:

//------ lib.js ------
var sqrt = Math.sqrt;
function square(x) {
    return x * x;
}
function diag(x, y) {
    return sqrt(square(x) + square(y));
}
module.exports = {
    sqrt: sqrt,
    square: square,
    diag: diag,
};

//------ main.js ------
var square = require('lib').square;
var diag = require('lib').diag;
console.log(square(11)); // 121
console.log(diag(4, 3)); // 5

Default exports (one per module)

Modules that only export single values are very popular in the Node.js community. But they are also common in frontend development where you often have constructors/classes for models, with one model per module. An ECMAScript 6 module can pick a default export, the most important exported value. Default exports are especially easy to import.

The following ECMAScript 6 module “is” a single function:

//------ myFunc.js ------
export default function () { ... };

//------ main1.js ------
import myFunc from 'myFunc';
myFunc();

An ECMAScript 6 module whose default export is a class looks as follows:

//------ MyClass.js ------
export default class { ... };

//------ main2.js ------
import MyClass from 'MyClass';
let inst = new MyClass();

Note: The operand of the default export declaration is an expression, it often does not have a name. Instead, it is to be identified via its module’s name.

Having both named exports and a default export in a module

The following pattern is surprisingly common in JavaScript: A library is a single function, but additional services are provided via properties of that function. Examples include jQuery and Underscore.js. The following is a sketch of Underscore as a CommonJS module:

//------ underscore.js ------
var _ = function (obj) {
    ...
};
var each = _.each = _.forEach =
    function (obj, iterator, context) {
        ...
    };
module.exports = _;

//------ main.js ------
var _ = require('underscore');
var each = _.each;
...

With ES6 glasses, the function _ is the default export, while each and forEachare named exports. As it turns out, you can actually have named exports and a default export at the same time. As an example, the previous CommonJS module, rewritten as an ES6 module, looks like this:

//------ underscore.js ------
export default function (obj) {
    ...
};
export function each(obj, iterator, context) {
    ...
}
export { each as forEach };

//------ main.js ------
import _, { each } from 'underscore';
...

Note that the CommonJS version and the ECMAScript 6 version are only roughly similar. The latter has a flat structure, whereas the former is nested. Which style you prefer is a matter of taste, but the flat style has the advantage of being statically analyzable (why that is good is explained below). The CommonJS style seems partially motivated by the need for objects as namespaces, a need that can often be fulfilled via ES6 modules and named exports.

The default export is just another named export

The default export is actually just a named export with the special name default. That is, the following two statements are equivalent:

import { default as foo } from 'lib';
import foo from 'lib';

Similarly, the following two modules have the same default export:

//------ module1.js ------
export default 123;

//------ module2.js ------
const D = 123;
export { D as default };

Why do we need named exports?

You may be wondering – why do we need named exports if we could simply default-export objects (like CommonJS)? The answer is that you can’t enforce a static structure via objects and lose all of the associated advantages (described in the next section).

Design goals

If you want to make sense of ECMAScript 6 modules, it helps to understand what goals influenced their design. The major ones are:

  • Default exports are favored
  • Static module structure
  • Support for both synchronous and asynchronous loading
  • Support for cyclic dependencies between modules

The following subsections explain these goals.

Default exports are favored

The module syntax suggesting that the default export “is” the module may seem a bit strange, but it makes sense if you consider that one major design goal was to make default exports as convenient as possible. Quoting David Herman:

ECMAScript 6 favors the single/default export style, and gives the sweetest syntax to importing the default. Importing named exports can and even should be slightly less concise.

Static module structure

In current JavaScript module systems, you have to execute the code in order to find out what the imports and exports are. That is the main reason why ECMAScript 6 breaks with those systems: by building the module system into the language, you can syntactically enforce a static module structure. Let’s first examine what that means and then what benefits it brings.

A module’s structure being static means that you can determine imports and exports at compile time (statically) – you only have to look at the source code, you don’t have to execute it. The following are two examples of how CommonJS modules can make that impossible. In the first example, you have to run the code to find out what it imports:

var mylib;
if (Math.random()) {
    mylib = require('foo');
} else {
    mylib = require('bar');
}

In the second example, you have to run the code to find out what it exports:

if (Math.random()) {
    exports.baz = ...;
}

ECMAScript 6 gives you less flexibility, it forces you to be static. As a result, you get several benefits [2], which are described next.

Benefit 1: faster lookup

If you require a library in CommonJS, you get back an object:

var lib = require('lib');
lib.someFunc(); // property lookup

Thus, accessing a named export via lib.someFunc means you have to do a property lookup, which is slow, because it is dynamic.

In contrast, if you import a library in ES6, you statically know its contents and can optimize accesses:

import * as lib from 'lib';
lib.someFunc(); // statically resolved

Benefit 2: variable checking

With a static module structure, you always statically know which variables are visible at any location inside the module:

  • Global variables: increasingly, the only completely global variables will come from the language proper. Everything else will come from modules (including functionality from the standard library and the browser). That is, you statically know all global variables.
  • Module imports: You statically know those, too.
  • Module-local variables: can be determined by statically examining the module.

This helps tremendously with checking whether a given identifier has been spelled properly. This kind of check is a popular feature of linters such as JSLint and JSHint; in ECMAScript 6, most of it can be performed by JavaScript engines.

Additionally, any access of named imports (such as lib.foo) can also be checked statically.

Benefit 3: ready for macros

Macros are still on the roadmap for JavaScript’s future. If a JavaScript engine supports macros, you can add new syntax to it via a library. Sweet.js is an experimental macro system for JavaScript. The following is an example from the Sweet.js website: a macro for classes.

// Define the macro
macro class {
    rule {
        $className {
                constructor $cparams $cbody
                $($mname $mparams $mbody) ...
        }
    } => {
        function $className $cparams $cbody
        $($className.prototype.$mname
            = function $mname $mparams $mbody; ) ...
    }
}

// Use the macro
class Person {
    constructor(name) {
        this.name = name;
    }
    say(msg) {
        console.log(this.name + " says: " + msg);
    }
}
var bob = new Person("Bob");
bob.say("Macros are sweet!");

For macros, a JavaScript engine performs a preprocessing step before compilation: If a sequence of tokens in the token stream produced by the parser matches the pattern part of the macro, it is replaced by tokens generated via the body of macro. The preprocessing step only works if you are able to statically find macro definitions. Therefore, if you want to import macros via modules then they must have a static structure.

Benefit 4: ready for types

Static type checking imposes constraints similar to macros: it can only be done if type definitions can be found statically. Again, types can only be imported from modules if they have a static structure.

Types are appealing because they enable statically typed fast dialects of JavaScript in which performance-critical code can be written. One such dialect is Low-Level JavaScript (LLJS). It currently compiles to asm.js.

Benefit 5: supporting other languages

If you want to support compiling languages with macros and static types to JavaScript then JavaScript’s modules should have a static structure, for the reasons mentioned in the previous two sections.

Support for both synchronous and asynchronous loading

ECMAScript 6 modules must work independently of whether the engine loads modules synchronously (e.g. on servers) or asynchronously (e.g. in browsers). Its syntax is well suited for synchronous loading, asynchronous loading is enabled by its static structure: Because you can statically determine all imports, you can load them before evaluating the body of the module (in a manner reminiscent of AMD modules).

Support for cyclic dependencies between modules

Two modules A and B are cyclically dependent on each other if both A (possibly indirectly/transitively) imports B and B imports A. If possible, cyclic dependencies should be avoided, they lead to A and B being tightly coupled – they can only be used and evolved together.

Why support cyclic dependencies?

Cyclic dependencies are not inherently evil. Especially for objects, you sometimes even want this kind of dependency. For example, in some trees (such as DOM documents), parents refer to children and children refer back to parents. In libraries, you can usually avoid cyclic dependencies via careful design. In a large system, though, they can happen, especially during refactoring. Then it is very useful if a module system supports them, because then the system doesn’t break while you are refactoring.

The Node.js documentation acknowledges the importance of cyclic dependencies [3] and Rob Sayre provides additional evidence:

Data point: I once implemented a system like [ECMAScript 6 modules] for Firefox. I got asked for cyclic dependency support 3 weeks after shipping.

That system that Alex Fritze invented and I worked on is not perfect, and the syntax isn’t very pretty. But it’s still getting used 7 years later, so it must have gotten something right.

Let’s see how CommonJS and ECMAScript 6 handle cyclic dependencies.

Cyclic dependencies in CommonJS

In CommonJS, if a module B requires a module A whose body is currently being evaluated, it gets back A’s exports object in its current state (line #1 in the following example). That enables B to refer to properties of that object inside its exports (line #2). The properties are filled in after B’s evaluation is finished, at which point B’s exports work properly.

//------ a.js ------
var b = require('b');
exports.foo = function () { ... };

//------ b.js ------
var a = require('a'); // (1)
// Can’t use a.foo in module body,
// but it will be filled in later
exports.bar = function () {
    a.foo(); // OK (2)
};

//------ main.js ------
var a = require('a');

As a general rule, keep in mind that with cyclic dependencies, you can’t access imports in the body of the module. That is inherent to the phenomenon and doesn’t change with ECMAScript 6 modules.

The limitations of the CommonJS approach are:

  • Node.js-style single-value exports don’t work. In Node.js, you can export single values instead of objects, like this:
    module.exports = function () { ... }
    If you did that in module A, you wouldn’t be able to use the exported function in module B, because B’s variable a would still refer to A’s original exports object.
  • You can’t use named exports directly. That is, module B can’t import a.foolike this:
    var foo = require('a').foo;
    foo would simply be undefined. In other words, you have no choice but to refer to foo via the exports object a.

CommonJS has one unique feature: you can export before importing. Such exports are guaranteed to be accessible in the bodies of importing modules. That is, if A did that, they could be accessed in B’s body. However, exporting before importing is rarely useful.

Cyclic dependencies in ECMAScript 6

In order to eliminate the aforementioned two limitations, ECMAScript 6 modules export bindings, not values. That is, the connection to variables declared inside the module body remains live. This is demonstrated by the following code.

//------ lib.js ------
export let counter = 0;
export function inc() {
    counter++;
}

//------ main.js ------
import { inc, counter } from 'lib';
console.log(counter); // 0
inc();
console.log(counter); // 1

Thus, in the face of cyclic dependencies, it doesn’t matter whether you access a named export directly or via its module: There is an indirection involved in either case and it always works.

More on importing and exporting

Importing

ECMAScript 6 provides the following ways of importing [4]:

// Default exports and named exports
import theDefault, { named1, named2 } from 'src/mylib';
import theDefault from 'src/mylib';
import { named1, named2 } from 'src/mylib';

// Renaming: import named1 as myNamed1
import { named1 as myNamed1, named2 } from 'src/mylib';

// Importing the module as an object
// (with one property per named export)
import * as mylib from 'src/mylib';

// Only load the module, don’t import anything
import 'src/mylib';

Exporting

There are two ways in which you can export things that are inside the current module [5]. On one hand, you can mark declarations with the keyword export.

export var myVar1 = ...;
export let myVar2 = ...;
export const MY_CONST = ...;

export function myFunc() {
    ...
}
export function* myGeneratorFunc() {
    ...
}
export class MyClass {
    ...
}

The “operand” of a default export is an expression (including function expressions and class expressions). Examples:

export default 123;
export default function (x) {
    return x
};
export default x => x;
export default class {
    constructor(x, y) {
        this.x = x;
        this.y = y;
    }
};

On the other hand, you can list everything you want to export at the end of the module (which is once again similar in style to the revealing module pattern).

const MY_CONST = ...;
function myFunc() {
    ...
}

export { MY_CONST, myFunc };

You can also export things under different names:

export { MY_CONST as THE_CONST, myFunc as theFunc };

Note that you can’t use reserved words (such as default and new) as variable names, but you can use them as names for exports (you can also use them as property names in ECMAScript 5). If you want to directly import such named exports, you have to rename them to proper variables names.

Re-exporting

Re-exporting means adding another module’s exports to those of the current module. You can either add all of the other module’s exports:

export * from 'src/other_module';

Or you can be more selective (optionally while renaming):

export { foo, bar } from 'src/other_module';

// Export other_module’s foo as myFoo
export { foo as myFoo, bar } from 'src/other_module';

eval() and modules

eval() does not support module syntax. It parses its argument according to the Script grammar rule and scripts don’t support module syntax (why is explained later). If you want to evaluate module code, you can use the module loader API (described next).

The ECMAScript 6 module loader API

In addition to the declarative syntax for working with modules, there is also a programmatic API. It allows you to:

  • Programmatically work with modules and scripts
  • Configure module loading

Loaders handle resolving module specifiers (the string IDs at the end of import...from), loading modules, etc. Their constructor is Reflect.Loader. Each platform keeps a customized instance in the global variable System (the system loader), which implements its specific style of module loading.

Importing modules and loading scripts

You can programmatically import a module, via an API based on ES6 promises:

System.import('some_module')
.then(some_module => {
    // Use some_module
})
.catch(error => {
    ...
});

System.import() enables you to:

  • Use modules inside <script> elements (where module syntax is not supported, consult Sect. “Further information” for details).
  • Load modules conditionally.

System.import() retrieves a single module, you can use Promise.all() to import several modules:

Promise.all(
    ['module1', 'module2', 'module3']
    .map(x => System.import(x)))
.then(([module1, module2, module3]) => {
    // Use module1, module2, module3
});

More loader methods:

Configuring module loading

The module loader API has various hooks for configuration. It is still work in progress. A first system loader for browsers is currently being implemented and tested. The goal is to figure out how to best make module loading configurable.

The loader API will permit many customizations of the loading process. For example:

  1. Lint modules on import (e.g. via JSLint or JSHint).
  2. Automatically translate modules on import (they could contain CoffeeScript or TypeScript code).
  3. Use legacy modules (AMD, Node.js).

Configurable module loading is an area where Node.js and CommonJS are limited.

Further information

The following content answers two important questions related to ECMAScript 6 modules: How do I use them today? How do I embed them in HTML?

  • Using ECMAScript 6 today gives an overview of ECMAScript 6 and explains how to compile it to ECMAScript 5. If you are interested in the latter, start reading in Sect. 2. One intriguing minimal solution is the ES6 Module Transpiler which only adds ES6 module syntax to ES5 and compiles it to either AMD or CommonJS.
  • Embedding ES6 modules in HTML: The code inside <script> elements does not support module syntax, because the element’s synchronous nature is incompatible with the asynchronicity of modules. Instead, you need to use the new <module> element. The blog post “ECMAScript 6 modules in future browsers” explains how <module> works. It has several significant advantages over <script> and can be polyfilled in its alternative version <script type="module">.
  • CommonJS vs. ES6:JavaScript Modules” (by Yehuda Katz) is a quick intro to ECMAScript 6 modules. Especially interesting is a second page where CommonJS modules are shown side by side with their ECMAScript 6 versions.

Benefits of ECMAScript 6 modules

At first glance, having modules built into ECMAScript 6 may seem like a boring feature – after all, we already have several good module systems. But ECMAScript 6 modules have features that you can’t add via a library, such as a very compact syntax and a static module structure (which helps with optimizations, static checking and more). They will also – hopefully – end the fragmentation between the currently dominant standards CommonJS and AMD.

Having a single, native standard for modules means:

  • No more UMD (Universal Module Definition): UMD is a name for patterns that enable the same file to be used by several module systems (e.g. both CommonJS and AMD). Once ES6 is the only module standard, UMD becomes obsolete.
  • New browser APIs become modules instead of global variables or properties of navigator.
  • No more objects-as-namespaces: Objects such as Math and JSON serve as namespaces for functions in ECMAScript 5. In the future, such functionality can be provided via modules.

Acknowledgements: Thanks to Domenic Denicola for confirming the final module syntax. Thanks for corrections of this blog post go to: Guy Bedford, John K. Paul, Mathias Bynens, Michael Ficarra.

References


  1. A JavaScript glossary: ECMAScript, TC39, etc. ↩︎
  2. Static module resolution” by David Herman ↩︎
  3. Modules: Cycles” in the Node.js API documentation ↩︎
  4. Imports” (ECMAScript 6 specification) ↩︎
  5. Exports” (ECMAScript 6 specification) ↩︎