30. An overview of what’s new in ES6
Table of contents
Please support this book: buy it (PDF, EPUB, MOBI) or donate
(Ad, please don’t block.)

30. An overview of what’s new in ES6

This chapter collects the overview sections of all the chapters in this book.

30.1 Categories of ES6 features

The introduction of the ES6 specification lists all new features:

Some of [ECMAScript 6’s] major enhancements include modules, class declarations, lexical block scoping, iterators and generators, promises for asynchronous programming, destructuring patterns, and proper tail calls. The ECMAScript library of built-ins has been expanded to support additional data abstractions including maps, sets, and arrays of binary numeric values as well as additional support for Unicode supplemental characters in strings and regular expressions. The built-ins are now extensible via subclassing.

There are three major categories of features:

30.2 New number and Math features

30.2.1 New integer literals

You can now specify integers in binary and octal notation:

> 0xFF // ES5: hexadecimal
255
> 0b11 // ES6: binary
3
> 0o10 // ES6: octal
8

30.2.2 New Number properties

The global object Number gained a few new properties:

30.2.3 New Math methods

The global object Math has new methods for numerical, trigonometric and bitwise operations. Let’s look at four examples.

Math.sign() returns the sign of a number:

> Math.sign(-8)
-1
> Math.sign(0)
0
> Math.sign(3)
1

Math.trunc() removes the decimal fraction of a number:

> Math.trunc(3.1)
3
> Math.trunc(3.9)
3
> Math.trunc(-3.1)
-3
> Math.trunc(-3.9)
-3

Math.log10() computes the logarithm to base 10:

> Math.log10(100)
2

Math.hypot() Computes the square root of the sum of the squares of its arguments (Pythagoras’ theorem):

> Math.hypot(3, 4)
5    

30.3 New string features

New string methods:

> 'hello'.startsWith('hell')
true
> 'hello'.endsWith('ello')
true
> 'hello'.includes('ell')
true
> 'doo '.repeat(3)
'doo doo doo '

ES6 has a new kind of string literal, the template literal:

// String interpolation via template literals (in backticks)
const first = 'Jane';
const last = 'Doe';
console.log(`Hello ${first} ${last}!`);
    // Hello Jane Doe!

// Template literals also let you create strings with multiple lines
const multiLine = `
This is
a string
with multiple
lines`;

30.4 Symbols

Symbols are a new primitive type in ECMAScript 6. They are created via a factory function:

const mySymbol = Symbol('mySymbol');

Every time you call the factory function, a new and unique symbol is created. The optional parameter is a descriptive string that is shown when printing the symbol (it has no other purpose):

> mySymbol
Symbol(mySymbol)

30.4.1 Use case 1: unique property keys

Symbols are mainly used as unique property keys – a symbol never clashes with any other property key (symbol or string). For example, you can make an object iterable (usable via the for-of loop and other language mechanisms), by using the symbol stored in Symbol.iterator as the key of a method (more information on iterables is given in the chapter on iteration):

const iterableObject = {
    [Symbol.iterator]() { // (A)
        ···
    }
}
for (const x of iterableObject) {
    console.log(x);
}
// Output:
// hello
// world

In line A, a symbol is used as the key of the method. This unique marker makes the object iterable and enables us to use the for-of loop.

30.4.2 Use case 2: constants representing concepts

In ECMAScript 5, you may have used strings to represent concepts such as colors. In ES6, you can use symbols and be sure that they are always unique:

const COLOR_RED    = Symbol('Red');
const COLOR_ORANGE = Symbol('Orange');
const COLOR_YELLOW = Symbol('Yellow');
const COLOR_GREEN  = Symbol('Green');
const COLOR_BLUE   = Symbol('Blue');
const COLOR_VIOLET = Symbol('Violet');

function getComplement(color) {
    switch (color) {
        case COLOR_RED:
            return COLOR_GREEN;
        case COLOR_ORANGE:
            return COLOR_BLUE;
        case COLOR_YELLOW:
            return COLOR_VIOLET;
        case COLOR_GREEN:
            return COLOR_RED;
        case COLOR_BLUE:
            return COLOR_ORANGE;
        case COLOR_VIOLET:
            return COLOR_YELLOW;
        default:
            throw new Exception('Unknown color: '+color);
    }
}

Every time you call Symbol('Red'), a new symbol is created. Therefore, COLOR_RED can never be mistaken for another value. That would be different if it were the string 'Red'.

30.4.3 Pitfall: you can’t coerce symbols to strings

Coercing (implicitly converting) symbols to strings throws exceptions:

const sym = Symbol('desc');

const str1 = '' + sym; // TypeError
const str2 = `${sym}`; // TypeError

The only solution is to convert explicitly:

const str2 = String(sym); // 'Symbol(desc)'
const str3 = sym.toString(); // 'Symbol(desc)'

Forbidding coercion prevents some errors, but also makes working with symbols more complicated.

The following operations are aware of symbols as property keys:

The following operations ignore symbols as property keys:

30.5 Template literals

ES6 has two new kinds of literals: template literals and tagged template literals. These two literals have similar names and look similar, but they are quite different. It is therefore important to distinguish:

Template literals are string literals that can stretch across multiple lines and include interpolated expressions (inserted via ${···}):

const firstName = 'Jane';
console.log(`Hello ${firstName}!
How are you
today?`);

// Output:
// Hello Jane!
// How are you
// today?

Tagged template literals (short: tagged templates) are created by mentioning a function before a template literal:

> String.raw`A \tagged\ template`
'A \\tagged\\ template'

Tagged templates are function calls. In the previous example, the method String.raw is called to produce the result of the tagged template.

30.6 Variables and scoping

ES6 provides two new ways of declaring variables: let and const, which mostly replace the ES5 way of declaring variables, var.

30.6.1 let

let works similarly to var, but the variable it declares is block-scoped, it only exists within the current block. var is function-scoped.

In the following code, you can see that the let-declared variable tmp only exists inside the block that starts in line A:

function order(x, y) {
    if (x > y) { // (A)
        let tmp = x;
        x = y;
        y = tmp;
    }
    console.log(tmp===x); // ReferenceError: tmp is not defined
    return [x, y];
}

30.6.2 const

const works like let, but the variable you declare must be immediately initialized, with a value that can’t be changed afterwards.

const foo;
    // SyntaxError: missing = in const declaration

const bar = 123;
bar = 456;
    // TypeError: `bar` is read-only

Since for-of creates one binding (storage space for a variable) per loop iteration, it is OK to const-declare the loop variable:

for (const x of ['a', 'b']) {
    console.log(x);
}
// Output:
// a
// b

30.6.3 Ways of declaring variables

The following table gives an overview of six ways in which variables can be declared in ES6 (inspired by a table by kangax):

  Hoisting Scope Creates global properties
var Declaration Function Yes
let Temporal dead zone Block No
const Temporal dead zone Block No
function Complete Block Yes
class No Block No
import Complete Module-global No

30.7 Destructuring

Destructuring is a convenient way of extracting values from data stored in (possibly nested) objects and Arrays. It can be used in locations that receive data (such as the left-hand side of an assignment). How to extract the values is specified via patterns (read on for examples).

30.7.1 Object destructuring

Destructuring objects:

const obj = { first: 'Jane', last: 'Doe' };
const {first: f, last: l} = obj;
    // f = 'Jane'; l = 'Doe'

// {prop} is short for {prop: prop}
const {first, last} = obj;
    // first = 'Jane'; last = 'Doe'

Destructuring helps with processing return values:

const obj = { foo: 123 };

const {writable, configurable} =
    Object.getOwnPropertyDescriptor(obj, 'foo');

console.log(writable, configurable); // true true

30.7.2 Array destructuring

Array destructuring (works for all iterable values):

const iterable = ['a', 'b'];
const [x, y] = iterable;
    // x = 'a'; y = 'b'

Destructuring helps with processing return values:

const [all, year, month, day] =
    /^(\d\d\d\d)-(\d\d)-(\d\d)$/
    .exec('2999-12-31');

30.7.3 Where can destructuring be used?

Destructuring can be used in the following locations:

// Variable declarations:
const [x] = ['a'];
let [x] = ['a'];
var [x] = ['a'];

// Assignments:
[x] = ['a'];

// Parameter definitions:
function f([x]) { ··· }
f(['a']);

You can also destructure in a for-of loop:

const arr1 = ['a', 'b'];
for (const [index, element] of arr1.entries()) {
    console.log(index, element);
}
// Output:
// 0 a
// 1 b

const arr2 = [
    {name: 'Jane', age: 41},
    {name: 'John', age: 40},
];
for (const {name, age} of arr2) {
    console.log(name, age);
}
// Output:
// Jane 41
// John 40

30.8 Parameter handling

Parameter handling has been significantly upgraded in ECMAScript 6. It now supports parameter default values, rest parameters (varargs) and destructuring.

Default parameter values:

function findClosestShape(x=0, y=0) {
    // ...
}

Rest parameters:

function format(pattern, ...params) {
    return params;
}
console.log(format('a', 'b', 'c')); // ['b', 'c']

Named parameters via destructuring:

function selectEntries({ start=0, end=-1, step=1 } = {}) {
    // The object pattern is an abbreviation of:
    // { start: start=0, end: end=-1, step: step=1 }

    // Use the variables `start`, `end` and `step` here
    ···
}

selectEntries({ start: 10, end: 30, step: 2 });
selectEntries({ step: 3 });
selectEntries({});
selectEntries();

30.8.1 Spread operator (...)

In function and constructor calls, the spread operator turns iterable values into arguments:

> Math.max(-1, 5, 11, 3)
11
> Math.max(...[-1, 5, 11, 3])
11
> Math.max(-1, ...[-1, 5, 11], 3)
11

In Array literals, the spread operator turns iterable values into Array elements:

> [1, ...[2,3], 4]
[1, 2, 3, 4]

30.9 Callable entities in ECMAScript 6

In ES5, a single construct, the (traditional) function, played three roles:

In ES6, there is more specialization. The three duties are now handled as follows (a class definition is either one of the two constructs for creating classes – a class declaration or a class expression):

This list is a simplification. There are quite a few libraries that use this as an implicit parameter for callbacks. Then you have to use traditional functions.

Note that I distinguish:

Even though their behaviors differ (as explained later), all of these entities are functions. For example:

> typeof (() => {}) // arrow function
'function'
> typeof function* () {} // generator function
'function'
> typeof class {} // class
'function'

30.10 Arrow functions

There are two benefits to arrow functions.

First, they are less verbose than traditional function expressions:

const arr = [1, 2, 3];
const squares = arr.map(x => x * x);

// Traditional function expression:
const squares = arr.map(function (x) { return x * x });

Second, their this is picked up from surroundings (lexical). Therefore, you don’t need bind() or that = this, anymore.

function UiComponent() {
    const button = document.getElementById('myButton');
    button.addEventListener('click', () => {
        console.log('CLICK');
        this.handleClick(); // lexical `this`
    });
}

The following variables are all lexical inside arrow functions:

30.11 New OOP features besides classes

30.11.1 New object literal features

Method definitions:

const obj = {
    myMethod(x, y) {
        ···
    }
};

Property value shorthands:

const first = 'Jane';
const last = 'Doe';

const obj = { first, last };
// Same as:
const obj = { first: first, last: last };

Computed property keys:

const propKey = 'foo';
const obj = {
    [propKey]: true,
    ['b'+'ar']: 123
};

This new syntax can also be used for method definitions:

const obj = {
    ['h'+'ello']() {
        return 'hi';
    }
};
console.log(obj.hello()); // hi

The main use case for computed property keys is to make it easy to use symbols as property keys.

30.11.2 New methods in Object

The most important new method of Object is assign(). Traditionally, this functionality was called extend() in the JavaScript world. In contrast to how this classic operation works, Object.assign() only considers own (non-inherited) properties.

const obj = { foo: 123 };
Object.assign(obj, { bar: true });
console.log(JSON.stringify(obj));
    // {"foo":123,"bar":true}

30.12 Classes

A class and a subclass:

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;
    }
}

Using the classes:

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

> cp.toString();
'(25, 8) in green'

> cp instanceof ColorPoint
true
> cp instanceof Point
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:

> typeof Point
'function'

30.13 Modules

JavaScript has had modules for a long time. However, they were implemented via libraries, not built into the language. ES6 is the first time that JavaScript has built-in modules.

ES6 modules are stored in files. There is exactly one module per file and one file per module. You have two ways of exporting things from a module. These two ways can be mixed, but it is usually better to use them separately.

30.13.1 Multiple named exports

There can be multiple 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

You can also import the complete module:

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

30.13.2 Single default export

There can be a single default export. For example, a function:

//------ myFunc.js ------
export default function () { ··· } // no semicolon!

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

Or a class:

//------ MyClass.js ------
export default class { ··· } // no semicolon!

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

Note that there is no semicolon at the end if you default-export a function or a class (which are anonymous declarations).

30.13.3 Browsers: scripts versus modules

  Scripts Modules
HTML element <script> <script type="module">
Default mode non-strict strict
Top-level variables are global local to module
Value of this at top level window undefined
Executed synchronously asynchronously
Declarative imports (import statement) no yes
Programmatic imports (Promise-based API) yes yes
File extension .js .js

30.14 The for-of loop

for-of is a new loop in ES6 that replaces both for-in and forEach() and supports the new iteration protocol.

Use it to loop over iterable objects (Arrays, strings, Maps, Sets, etc.; see Chap. “Iterables and iterators”):

const iterable = ['a', 'b'];
for (const x of iterable) {
    console.log(x);
}

// Output:
// a
// b

break and continue work inside for-of loops:

for (const x of ['a', '', 'b']) {
    if (x.length === 0) break;
    console.log(x);
}

// Output:
// a

Access both elements and their indices while looping over an Array (the square brackets before of mean that we are using destructuring):

const arr = ['a', 'b'];
for (const [index, element] of arr.entries()) {
    console.log(`${index}. ${element}`);
}

// Output:
// 0. a
// 1. b

Looping over the [key, value] entries in a Map (the square brackets before of mean that we are using destructuring):

const map = new Map([
    [false, 'no'],
    [true, 'yes'],
]);
for (const [key, value] of map) {
    console.log(`${key} => ${value}`);
}

// Output:
// false => no
// true => yes

30.15 New Array features

New static Array methods:

New Array.prototype methods:

30.16 Maps and Sets

Among others, the following four data structures are new in ECMAScript 6: Map, WeakMap, Set and WeakSet.

30.16.1 Maps

The keys of a Map can be arbitrary values:

> const map = new Map(); // create an empty Map
> const KEY = {};

> map.set(KEY, 123);
> map.get(KEY)
123
> map.has(KEY)
true
> map.delete(KEY);
true
> map.has(KEY)
false

You can use an Array (or any iterable) with [key, value] pairs to set up the initial data in the Map:

const map = new Map([
    [ 1, 'one' ],
    [ 2, 'two' ],
    [ 3, 'three' ], // trailing comma is ignored
]);

30.16.2 Sets

A Set is a collection of unique elements:

const arr = [5, 1, 5, 7, 7, 5];
const unique = [...new Set(arr)]; // [ 5, 1, 7 ]

As you can see, you can initialize a Set with elements if you hand the constructor an iterable (arr in the example) over those elements.

30.16.3 WeakMaps

A WeakMap is a Map that doesn’t prevent its keys from being garbage-collected. That means that you can associate data with objects without having to worry about memory leaks. For example:

//----- Manage listeners

const _objToListeners = new WeakMap();

function addListener(obj, listener) {
    if (! _objToListeners.has(obj)) {
        _objToListeners.set(obj, new Set());
    }
    _objToListeners.get(obj).add(listener);
}

function triggerListeners(obj) {
    const listeners = _objToListeners.get(obj);
    if (listeners) {
        for (const listener of listeners) {
            listener();
        }
    }
}

//----- Example: attach listeners to an object

const obj = {};
addListener(obj, () => console.log('hello'));
addListener(obj, () => console.log('world'));

//----- Example: trigger listeners

triggerListeners(obj);

// Output:
// hello
// world

30.17 Typed Arrays

Typed Arrays are an ECMAScript 6 API for handling binary data.

Code example:

const typedArray = new Uint8Array([0,1,2]);
console.log(typedArray.length); // 3
typedArray[0] = 5;
const normalArray = [...typedArray]; // [5,1,2]

// The elements are stored in typedArray.buffer.
// Get a different view on the same data:
const dataView = new DataView(typedArray.buffer);
console.log(dataView.getUint8(0)); // 5

Instances of ArrayBuffer store the binary data to be processed. Two kinds of views are used to access the data:

The following browser APIs support Typed Arrays (details are mentioned in a dedicated section):

30.18 Iterables and iterators

ES6 introduces a new mechanism for traversing data: iteration. Two concepts are central to iteration:

Expressed as interfaces in TypeScript notation, these roles look like this:

interface Iterable {
    [Symbol.iterator]() : Iterator;
}
interface Iterator {
    next() : IteratorResult;
}
interface IteratorResult {
    value: any;
    done: boolean;
}

30.18.1 Iterable values

The following values are iterable:

Plain objects are not iterable (why is explained in a dedicated section).

30.18.2 Constructs supporting iteration

Language constructs that access data via iteration:

30.19 Generators

30.19.1 What are generators?

You can think of generators as processes (pieces of code) that you can pause and resume:

function* genFunc() {
    // (A)
    console.log('First');
    yield;
    console.log('Second');
}

Note the new syntax: function* is a new “keyword” for generator functions (there are also generator methods). yield is an operator with which a generator can pause itself. Additionally, generators can also receive input and send output via yield.

When you call a generator function genFunc(), you get a generator object genObj that you can use to control the process:

const genObj = genFunc();

The process is initially paused in line A. genObj.next() resumes execution, a yield inside genFunc() pauses execution:

genObj.next();
// Output: First
genObj.next();
// output: Second

30.19.2 Kinds of generators

There are four kinds of generators:

  1. Generator function declarations:
     function* genFunc() { ··· }
     const genObj = genFunc();
    
  2. Generator function expressions:
     const genFunc = function* () { ··· };
     const genObj = genFunc();
    
  3. Generator method definitions in object literals:
     const obj = {
         * generatorMethod() {
             ···
         }
     };
     const genObj = obj.generatorMethod();
    
  4. Generator method definitions in class definitions (class declarations or class expressions):
     class MyClass {
         * generatorMethod() {
             ···
         }
     }
     const myInst = new MyClass();
     const genObj = myInst.generatorMethod();
    

30.19.3 Use case: implementing iterables

The objects returned by generators are iterable; each yield contributes to the sequence of iterated values. Therefore, you can use generators to implement iterables, which can be consumed by various ES6 language mechanisms: for-of loop, spread operator (...), etc.

The following function returns an iterable over the properties of an object, one [key, value] pair per property:

function* objectEntries(obj) {
    const propKeys = Reflect.ownKeys(obj);

    for (const propKey of propKeys) {
        // `yield` returns a value and then pauses
        // the generator. Later, execution continues
        // where it was previously paused.
        yield [propKey, obj[propKey]];
    }
}

objectEntries() is used like this:

const jane = { first: 'Jane', last: 'Doe' };
for (const [key,value] of objectEntries(jane)) {
    console.log(`${key}: ${value}`);
}
// Output:
// first: Jane
// last: Doe

How exactly objectEntries() works is explained in a dedicated section. Implementing the same functionality without generators is much more work.

30.19.4 Use case: simpler asynchronous code

You can use generators to tremendously simplify working with Promises. Let’s look at a Promise-based function fetchJson() and how it can be improved via generators.

function fetchJson(url) {
    return fetch(url)
    .then(request => request.text())
    .then(text => {
        return JSON.parse(text);
    })
    .catch(error => {
        console.log(`ERROR: ${error.stack}`);
    });
}

With the library co and a generator, this asynchronous code looks synchronous:

const fetchJson = co.wrap(function* (url) {
    try {
        let request = yield fetch(url);
        let text = yield request.text();
        return JSON.parse(text);
    }
    catch (error) {
        console.log(`ERROR: ${error.stack}`);
    }
});

ECMAScript 2017 will have async functions which are internally based on generators. With them, the code looks like this:

async function fetchJson(url) {
    try {
        let request = await fetch(url);
        let text = await request.text();
        return JSON.parse(text);
    }
    catch (error) {
        console.log(`ERROR: ${error.stack}`);
    }
}

All versions can be invoked like this:

fetchJson('http://example.com/some_file.json')
.then(obj => console.log(obj));

30.19.5 Use case: receiving asynchronous data

Generators can receive input from next() via yield. That means that you can wake up a generator whenever new data arrives asynchronously and to the generator it feels like it receives the data synchronously.

30.20 New regular expression features

The following regular expression features are new in ECMAScript 6:

30.21 Promises for asynchronous programming

Promises are an alternative to callbacks for delivering the results of an asynchronous computation. They require more effort from implementors of asynchronous functions, but provide several benefits for users of those functions.

The following function returns a result asynchronously, via a Promise:

function asyncFunc() {
    return new Promise(
        function (resolve, reject) {
            ···
            resolve(result);
            ···
            reject(error);
        });
}

You call asyncFunc() as follows:

asyncFunc()
.then(result => { ··· })
.catch(error => { ··· });

30.21.1 Chaining then() calls

then() always returns a Promise, which enables you to chain method calls:

asyncFunc1()
.then(result1 => {
    // Use result1
    return asyncFunction2(); // (A)
})
.then(result2 => { // (B)
    // Use result2
})
.catch(error => {
    // Handle errors of asyncFunc1() and asyncFunc2()
});

How the Promise P returned by then() is settled depends on what its callback does:

Furthermore, note how catch() handles the errors of two asynchronous function calls (asyncFunction1() and asyncFunction2()). That is, uncaught errors are passed on until there is an error handler.

30.21.2 Executing asynchronous functions in parallel

If you chain asynchronous function calls via then(), they are executed sequentially, one at a time:

asyncFunc1()
.then(() => asyncFunc2());

If you don’t do that and call all of them immediately, they are basically executed in parallel (a fork in Unix process terminology):

asyncFunc1();
asyncFunc2();

Promise.all() enables you to be notified once all results are in (a join in Unix process terminology). Its input is an Array of Promises, its output a single Promise that is fulfilled with an Array of the results.

Promise.all([
    asyncFunc1(),
    asyncFunc2(),
])
.then(([result1, result2]) => {
    ···
})
.catch(err => {
    // Receives first rejection among the Promises
    ···
});

30.21.3 Glossary: Promises

The Promise API is about delivering results asynchronously. A Promise object (short: Promise) is a stand-in for the result, which is delivered via that object.

States:

Reacting to state changes:

Changing states: There are two operations for changing the state of a Promise. After you have invoked either one of them once, further invocations have no effect.

30.22 Metaprogramming with proxies

Proxies enable you to intercept and customize operations performed on objects (such as getting properties). They are a metaprogramming feature.

In the following example, proxy is the object whose operations we are intercepting and handler is the object that handles the interceptions. In this case, we are only intercepting a single operation, get (getting properties).

const target = {};
const handler = {
    get(target, propKey, receiver) {
        console.log('get ' + propKey);
        return 123;
    }
};
const proxy = new Proxy(target, handler);

When we get the property proxy.foo, the handler intercepts that operation:

> proxy.foo
get foo
123

Consult the reference for the complete API for a list of operations that can be intercepted.

Next: Notes