Using ECMAScript 6 features today

Reading Time: 4 minutes

ECMAScript 2015 (6th edition, commonly referred to as "ES6") is the current version of the ECMAScript standard, and is a significant update to the language. These features are currently being implemented in major JavaScript engines, but why wait when you can make use of all the awesome today?

I've been keen to use many of ES6's features in my day-to-day development, especially the modules syntax, and destructuring – which is particularly useful in doing away with the option object cruft that JavaScript developers have become used to writing in lieu of native support for named parameters.

Destructuring allows us to take something like this:


function pager(options) {

    if (!options) options = {};

    if (!options.min) options.min = 0;

    if (!options.max) options.max = 10;

    // use options.min and options.max

}

… and write it far more succinctly:


function pager({ min = 0, max = 10 } = {}) {

    // use 'min' and 'max'

}

This post details how Atlassian account (our single sign-on service) transitioned to using ES6 modules, and what we use to provide a full ES6 environment for development.

ES6 features

Here's a brief look at the juicy goodness ES6 has to offer, courtesy of Luke Hoban's excellent ES6 features overview (recommended reading for in-depth descriptions and examples):

  • arrows

  • classes

  • enhanced object literals

  • template strings

  • destructuring

  • default + rest + spread

  • let + const

  • iterators + for..of

  • generators

  • unicode

  • modules

  • module loaders

  • map + set + weakmap + weakset

  • proxies

  • symbols

  • subclassable built-ins

  • promises

  • math + number + string + object APIs

  • binary and octal literals

  • reflect api

  • tail calls

ES6 modules

Up until this point JavaScript has not had native support for modules and so the community has come up with some pretty nice work-arounds, the most popular being Asynchronous Module Definition (AMD) – designed for asynchronous loading of modules and dependencies in the browser – and CommonJS – the most common implementation being in Node.

ES6 modules took the best of both worlds and came up with syntax which should make both camps happy (and lends itself nicely to static analysis to boot).

However, because of the lack of browser support for ES6 modules we still need to compile to something the browsers can understand in the meantime. For us, that meant using AMD – although I've been experimenting with SystemJS (and jspm, built on top of it) to potentially do away with the need to convert the modules to AMD.

The environment

The two main ES6 to ES5 transpilers are Babel (formerly named "6to5") and Google's Traceur. We chose to use Babel for a few reasons:

  1. Traceur requires a heavyweight runtime, Babel only requires a few optional polyfills

  2. the code generated by Babel is closer to the original source

  3. Babel is a little more feature complete

  4. the web community's engagement with Babel is massive, and activity is extremely high

Atlassian account's implementation

Atlassian account is a relatively new code-base, so from the outset we've been able to use a modern front-end stack, harnessing all the power that Node and npm provide:

With this toolchain we use Babel via the grunt-babel plugin, transpiling our ES6 modules down to ES5 compatible AMD modules with Babel's AMD module formatter. The generated code ended up looking more or less like what we already had, but now we were using ES6 modules under the hood!

Grunt + Babel

Using the grunt-babel plugin it was trivial to use a glob expression in the Grunt configuration to specify the ES6 modules we wanted transpiled to ES5 compatible AMD modules.

Polyfill

Babel transpiles most of your code down to being ES5 compatible, but in order to avoid lots of duplication it relies on the core-js polyfill for much of the newer functionality, and a fork of Facebook's regenerator to provide support for generator functions. Babel bundles this as a single browser-polyfill.js that you include before any of your compiled code. I included it just after the RequireJS config in both our base template and our Karma configuration.

Attempting to include browser-polyfill.js more than once will throw an error, so ensure it only gets included once.

While the default Babel polyfill does an excellent job at polyfilling ECMAScript 6 features, we had a few issues with some of the other things it polyfills:

  • ECMAScript 5 features, which are unnecessary for the minimum browser versions we support

  • ECMAScript 7 proposals, which are quite likely to change or be dropped entirely

  • non-standard language features (e.g. window.setImmediate)

For this reason I built a very simple drop-in replacement for Babel's polyfill (which still uses core-js and Babel's regenerator fork), babel-es6-polyfill, that only includes the ES6 polyfills.

Source maps

When developing it's always nice to be able to debug your un-minified, un-batched source code and source maps provide a convenient way of being able to do that. Thankfully, with Babel this is as trivial as setting the sourceMap option and you're done. Depending on the folder structure of your project you may also want to set the sourceFileName option, too, so that you don't have to expand a bunch of folders in the browser's dev tools to see the original source.

Karma integration

In converting over our tests to ES6 I used the karma-babel-preprocessor. Configuring Karma (via the configuration file, usually named karma.conf.js or similar) to preprocess your tests with Babel before execution is straightforward:


module.exports = function (config) {

    config.set({

        preprocessors: {

            'src/**/*.js': ['babel'],

            'test/**/*.js': ['babel']

        },

        babelPreprocessor: {

            options: {

                sourceMap: 'inline',

                modules: 'amd'

            }

        },

        files: [

            'src/require-config.js',

            'src/lib/babel-es6-polyfill/browser-polyfill.js',

            'test/test-main.js',

            { pattern: 'src/**/*.js', included: false },

            { pattern: 'test/**/*.js', included: false }

        ]

    });

};

You'll notice that we've enabled source maps for the tests too, which will help if you find the need to debug them.

ESLint

Given that JavaScript is a dynamic and loosely-typed language, it is especially prone to developer error. In order to mitigate this we use the static analysis utility, ESLint, to find problematic patterns or code that breaks certain conventions and guidelines in our codebase.

ESLint allows you to specify environments that define which global variables are predefined and which rules should be on or off by default, as well as individual language options by use of the ecmaFeatures property. To enable a full ES6 environment requires a straightforward configuration:


{

  "env": {

    "es6": true // enable all ECMAScript 6 features except for modules

  },

  "ecmaFeatures": {

    "modules": true // enable modules and global strict mode

  }

}

The future is now

We may not have hover-boards yet, but we can use ES6 today. What are you waiting for? 😉