2014-08-01

Using ECMAScript 6 today

Updates:

  • [2014-11-05] Restructured the post and added more content (terminology, let, promises, ES7+).
  • [2014-09-02] Rewrote Sect. “More material on ECMAScript 6”; mentioned iterators, generators and promises.

ECMAScript 6 (ES6) still sounds like something from a far-away future. After all, it will only become a standard by mid 2015. However, its features are continually appearing in browsers and there are compilers that translate ES6 code to ES5 code. The latter is already a compelling solution, because the ECMAScript 6 feature set is already frozen.

This blog post gives a brief overview of ECMAScript 6 features and describes tools that enable you to use them today.

Terminology

  • TC39 (Ecma Technical Committee 39): the committee evolving JavaScript.
    • Members: companies (all major browser vendors etc.).
    • Meetings: attended by employees and invited experts.
  • ECMAScript: the official name of the language
    • Versions: ECMAScript 5 is short for “ECMAScript Language Specification, Edition 5”
  • JavaScript:
    • colloquially: the language
    • formally: one implementation of ECMAScript
  • ECMAScript Harmony: improvements after ECMAScript 5 (ECMAScript 6 and 7)
  • ECMAScript.next: code name for ECMAScript 6 (until version number and feature set was clearer)

More information:

ECMAScript 6 highlights

This section gives an overview of most ES6 features.

New syntax

Objects, functions, modules

Object literals – property value shorthand (used for destructuring in later examples):

    let first = 'Jane';
    let last = 'Doe';
    
    let obj = { first, last };
    // Same as:
    let obj = { first: first, last: last };

Object literals – method definitions:

    let obj = {
        myMethod(arg0, arg1) {
            ...
        }
    };

Arrow functions:

    // More compact syntax
    let arr = [1, 2, 3];
    let squares = arr.map(x => x * x);
    
    // `this` is picked up from surroundings (lexical)
    // Therefore: no more `that = this` or bind()
    function UiComponent {
        let button = document.getElementById('myButton');
        button.addEventListener('click', () => {
            console.log('CLICK');
            this.handleClick(); // lexical `this`
        });
    }

Classes:

    // Superclass
    class Person {
        constructor(name) {
            this.name = name;
        }
        describe() {
            return 'Person called ' + this.name;
        }
    }
    
    // Subclass
    class Employee extends Person {
        constructor(name, title) {
            super(name); 
            this.title = title;
        }
        describe() {
            return super.describe() + ' (' + this.title + ')';
        }
    }

Subclassing built-ins such as Error and Array:

    class MyError extends Error {
        // ...
    }

Modules:

    // 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
Variables, assignment and parameter handling

Block-scoped variables via let (writable) and const (read-only):

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

Spread operator:

    let arr = [-1, 7, 2];
    let highest = Math.max(...arr); // 7
    
    new Date(...[2011, 11, 24]) // 2011, December 24, 00:00
    
    // Non-destructively concatenate single elements
    let arr2 = [...arr, 9, -6]; // [-1, 7, 2, 9, -6]

Destructuring:

    let [all, year, month, day] =
        /^(\d\d\d\d)-(\d\d)-(\d\d)$/
        .exec('2999-12-31');
    
    let { first, last } = { first: 'Jane', last: 'Doe' };

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:

    class Entries {
        // ...
        selectEntries({ from = 0, to = this.length } = {}) {
        // Long: { from: from=0, to: to=this.length }
    
            // Use `from` and `to`
        }
    }
    let entries = new Entries();
    entries.selectEntries({ from: 5, to: 15 });
    entries.selectEntries({ from: 5 });
    entries.selectEntries({ to: 15 });
    entries.selectEntries({});
    entries.selectEntries();
Loops, iteration, generators

for-of loop (works for all objects that implement the ES6 iteration protocol):

    let arr = ['foo', 'bar', 'baz'];
    
    for (let element of arr) {
        console.log(element);
    }
    /* Output:
       foo
       bar
       baz
    */
    
    for (let [index,element] of arr.entries()) {
        console.log(`${index}. ${element}`);
    }
    /* Output:
       0. foo
       1. bar
       2. baz
    */

Iterators and generators (iterators are a protocol for retrieving the contents of a collection which is supported by the for-of loop; generators are “pausable functions” that help with implementing iterators and more):

    // Generator function, implementing an iterator over objects
    function* objectEntries(obj) {
        // In ES6, you can use strings or symbols as property keys,
        // Reflect.ownKeys() retrieves both
        for (let key of Reflect.ownKeys(obj)) {
            yield [key, obj[key]]; // pause and return a value
        }
    }
    
    let obj = { first: 'Jane', last: 'Doe' };
    
    for (let [key,value] of objectEntries(obj)) {
        console.log(`${key}. ${value}`);
    }
    /* Output:
       first. Jane
       last. Doe
    */
Template strings

Template strings:

    let str = String.raw`This is a text
    with multiple lines.
    Escapes are not interpreted,
    \n is not a newline.`;
    
    
    // XRegExp library
    var parts = '/2012/10/Page.html'.match(XRegExp.rx`
        ^ # match at start of string only
        / (?<year> [^/]+ ) # capture top dir name as year
        / (?<month> [^/]+ ) # capture subdir name as month
        / (?<title> [^/]+ ) # capture base name as title
        \.html? $ # .htm or .html file ext at end of path
    `);
    console.log(parts.year); // 2012

New functionality in the standard library

Utility methods

Object.assign():

    class Point {
        constructor(x, y) {
            Object.assign(this, { x, y });
            // ES6: { x, y } is abbrev. for { x: x, y: y }
        }
    }

Array.prototype.findIndex():

    > [6, 8, -5].findIndex(x => x < 0)
    2
    > [6, 8, 5].findIndex(x => x < 0)
    -1

Array.prototype.fill():

    > ['a', 'b', 'c'].fill(7)
    [ 7, 7, 7 ]
    > new Array(3).fill(7)
    [ 7, 7, 7 ]

New string methods:

    > 'hello world'.startsWith('hello')
    true
    > '*'.repeat(5)
    '*****'
Collections

Map (whose keys can be arbitrary values):

    > let obj = {};
    > let map = new Map();
    
    > map.set(obj, 123);
    > map.get(obj)
    123
    > map.has(obj)
    true
    > map.delete(obj);
    true
    > map.has(obj)
    false

Set:

    let arr = [5, 1, 5, 7, 7, 5];
    let unique = [...new Set(arr)]; // [ 5, 1, 7 ]
Asynchronous programming via promises

Promises: an API that helps with asynchronous programming. Quoting “JavaScript Promises: There and back again” by Jake Archibald:

all new DOM APIs with async success/failure methods will use promises. This is happening already with Quota Management, Font Load Events, ServiceWorker, Web MIDI, Streams, and more.

The following is an example of using promises: a function httpGet() that retrieves a resource via HTTP GET (the current way of doing this is via XMLHttpRequest):

    httpGet('http://example.com/file.txt')
    .then(
        value => {
            console.log('Contents: ' + value);
        },
        reason => {
            console.error('Something went wrong', reason);
        });

This asynchronous function could be implemented like this:

    function httpGet(url) {
        return new Promise(
            function (resolve, reject) {
                var request = new XMLHttpRequest();
                request.onreadystatechange = function () {
                    if (this.status === 200) {
                        // Success
                        resolve(this.response);
                    } else {
                        // Something went wrong (404 etc.)
                        reject(new Error(this.statusText));
                    }
                }
                request.onerror = function () {
                    reject(new Error(
                        'XMLHttpRequest Error: '+this.statusText));
                };
                request.open('GET', url);
                request.send();    
            });
    }

Promises are explained in two blog posts:

More Material on ECMAScript 6

Using ECMAScript 6 today

ECMAScript 6 features are continually appearing in engines. You can look up which ones are already supported where in Kangax’ “ECMAScript 6 compatibility table”.

But for actual projects, you will probably use tools that enable ECMAScript 6 on current engines:

  • es6-tools” (by Addy Osmani) is a comprehensive list of such tools.
  • ES.next showcase” (by Sindre Sorhus) showcases real-world usage of ECMAScript 6 features. Two examples:
    • The next version of Ember.js supports ES6 modules via the ES6 Module Transpiler (see below).
    • The next version AngularJS supports ES6 via Traceur (see below).

The following sections describe a few ES6-enabling tools.

ECMAScript 6 compilers

If a tool transforms ECMAScript 6 code to ECMAScript 5 then its capabilities usually go beyond transpiling, which is why it is called a compiler. Two important compilers are TypeScript and Traceur.

TypeScript

It is the declared goal of TypeScript’s creators to track ECMAScript 6. Hence, the language gives you ECMAScript 6 plus type annotations (which are optional). TypeScript is easy to install via npm and supported by the IDEs Visual Studio and WebStorm.

TypeScript’s module syntax is currently a bit behind the ECMAScript 6 specification (something that will be fixed eventually). It supports two module standards: CJS (Node.js) and AMD (RequireJS).

Traceur

Traceur is the most popular pure ECMAScript 6 compiler. Its support for the new features is impressively complete. Traceur’s creators pronounce its name “tray-SOOR”. There are two ways in which you can use Traceur.

Statically: Traceur-based plugins for build tools (Grunt, Gulp, Broccoli, etc.) let you automatically compile ES6 files to ES5 files, during development. Consult es6-tools for details.

Dynamically: If you include Traceur in your web app then you can use it to compile ES6 code on the fly, by giving script tags the type="module".

    <!doctype html>
    <html>
    <head>
        <meta charset="UTF-8">
    </head>
    <body>
        <div id="output"></div>
    
        <script src="https://google.github.io/traceur-compiler/bin/traceur.js"></script>
        <script src="https://google.github.io/traceur-compiler/src/bootstrap.js"></script>
        <script type="module">
            var output = document.getElementById('output');
            var w = 'world';
            output.textContent = `Hello ${w}!`;
        </script>
    </body>
    </html>

You can tell Traceur via a compiler option what module standard the compiled ES5 output should use:

  • The ES6 module loader API (which Traceur supports on ES5 via a shim)
  • AMD (RequireJS)
  • CJS (Node.js)

Module systems and ECMAScript 6

Several existing and new JavaScript module systems support ECMAScript 6, sometimes out of the box, sometimes via a plugin.

  • Compiling to AMD and CJS: The ES6 Module Transpiler adds just the ECMAScript 6 module syntax to ECMAScript 5 and compiles it to either AMD or CJS. What I like about this solution is its minimalism, it works with existing setups.
  • Browserify: supports ES6 via the es6ify transform, which is based on Traceur.
  • webpack: comes out of the box with ECMAScript 6 support.
  • ES6 Module Loader Polyfill: is based on the ES6 API and “dynamically loads ES6 modules in Node.js and current browsers”. Complemented by two tools:
    • SystemJS: based on the ES6 module loader, loads AMD and CJS modules in addition to ES6 modules.
    • jspm.io: a package manager for SystemJS.

ECMAScript 6 command lines

JavaScript command lines are useful for interactively trying out features. This section describes command lines that accept ECMAScript 6.

ES6 Fiddle

ES6 Fiddle (GitHub repo) by Jeff McRiffey is an ECMAScript 6 command line base on Traceur. You can save an example under a URL with a unique ID by clicking one of the icons the toolbar.

Traceur transcoding demo

But Traceur also comes with its own interactive demo page. A few tips for using that page:

  • You can open the console and will see anything you log to it in the ES6 source code via console.log().
  • The ES6 source code is added to the page’s URL, which means that you can share ES6 examples via the URL.
  • Traceur implements a variety of ES6 methods in ES5, which means that you can use methods such as Array.from() from the console.
  • let and const variable declarations are still experimental and can be switched on via an option. However, the code that they produce is clunky. Thankfully, there are plans to vastly improve support for let and const: For example, Traceur will translate let to var in many cases. Thus, in those cases, there won’t be a penalty for using the (forward-looking) let.

ECMAScript 6 shims

Shims are libraries that bring features from future systems to current systems. The ECMAScript 6 standard library contains interesting new functionality, which often can be backported to ECMAScript 5 via libraries:

  • es6-shim (by Paul Millr): supports many features of the ECMAScript 6 standard library.
  • Shims for ECMAScript 6 promises (Traceur comes its own promise polyfill):
    • RSVP.js is a superset of the ES6 API.
      • es6-promise is a subset of RSVP.js and implements just the ES6 API.
    • Q.Promise is compatible with ES6.

ECMAScript 7 and later

Starting with ECMAScript 7, TC39 will time-box releases. Assuming a two-year interval (which seems likely), a new version of ECMAScript would be released every two years, with whatever features are ready at that time.

Work on ECMAScript 7+ has already begun:

  • Proposals are listed on GitHub.
  • A document describes the process that is used by TC39, starting with ES7, to create new ECMAScript versions.

Additionally, Microsoft, Facebook and Google are exploring optional static typing for JavaScript. Their approaches are lightweight and similar mechanisms may eventually become part of the language:

Does it still make sense to learn ECMAScript 5?

As we have seen, you can already exclusively write code in ECMAScript 6 and avoid older versions of JavaScript. Does that mean that you shouldn’t learn ECMAScript 5, anymore? Alas, it doesn’t, for several reasons:

  • ECMAScript 6 is a superset of ECMAScript 5 – new JavaScript versions must never break existing code. Thus, nothing you learn about ECMAScript 5 is learned in vain.

  • There are several ECMAScript 6 features that kind of replace ECMAScript 5 features, but still use them as their foundation. Two examples: classes are internally translated to constructors and methods are still functions (as they have always been).

  • As long as ECMAScript 6 is compiled to ECMAScript 5, it is useful to understand the output of the compilation process. And you’ll have to compile to ES5 for a while (probably years), until you can rely on ES6 being available in all relevant browsers, in the same manner in that you can usually rely on ES5 now.

  • It’s important to be able to understand legacy code.

5 comments:

Axel Rauschmayer said...

Thanks for this great article.

However, I wonder why your class example utilizes the this keyword to assign a property in the constructor.

Looking at http://wiki.ecmascript.org/doku.php?id=harmony:classes I see usage of the public and private keywords.

Axel Rauschmayer said...

This is a very old and outdated document. Everything that matters (w.r.t. ECMAScript 6) is in the spec draft now.

Axel Rauschmayer said...

In 2.1 sections "Classes" and "Subclassing built-ins" are repeated twice.

Axel Rauschmayer said...

Fixed, thanks!

Axel Rauschmayer said...

> Additionally, Microsoft, Facebook and Google are exploring optional static typing for JavaScript.

Apple seems to be conspicuously missing from many of these sorts of discussions these days.