The World of JavaScript Tomorrow, Today.

As much as I enjoy writing JavaScript, there are various parts of it I'm not the biggest fan of. When I saw Coffee-Script, I loved how much more enjoyable and easier it seemed to write JavaScript. But eventually there were places that Coffee-Script just seemed to get in the way or was overly fussy. I then started finding out about what the future holds for JavaScript, I liked what I saw and I wanted it now.

What the future holds for JavaScript

I won't delve into everything that ES6 holds, but I'll cover what caught my eye as the most interesting. Don't let me stop you researching into the other changes in ES6 as you might find something more applicable to your own work.

Classes

Class support has been signed off for ES6. It includes features such as constructors, inheritance (extends), instances, static methods and calling parent class methods (super.method()).

class Game {
  constructor(config) {
    this.config = config;
    this.players = [];
  }

  getType() {
    return this.config.type;
  }

  addPlayer(player) {
    this.players.push(player);
  }
}

Template Strings

These are used for constructing strings with variables and also add support for multiline strings.

var multi = `This is
a multiple lined string
in JavaScript`;

var mood = 'hate';
var output = 'I ' + mood + ' joining strings together';

var output = `I ${mood} joining strings together`;

Arrow functions

If you've used Coffee-Script, it's very similar. You can create a function as a statement or expression with the advantage of it being bound to the same this that you defined it in. This can be really helpful as this can be quite confusing sometimes unless you've bound it to something (.bind(this)).

var array = [1,2,3,4].map((v) => v * Math.PI);

var random = (num) => {
  return num * Math.random();
};

Object Literal improvements

Objects have had a couple of improvements added.

var name = 'bob';
var data = {name};
console.log(data);
// data = {"name": "bob"};

var obj = {
  method() {
    // Bound to `this`
  }
}

Destructing

This provides a nice way to pull out parts of an array or object.

var {readdir} = require('fs');
// Sets readdir to require('fs').readdir;

var [exec, script] = process.argv;

// Set a default if it doesn't exist
var [name = 'Dave'] = [];

Block scoping with let and const

Block scoping provides tighter control over your variables and constants. This can help when you want to avoid clashing variable names or changing values of a variable you've referenced in the wrong way.

{
  let name = 'Bob';
  console.log(name); // "Bob"
}
console.log(name); // Undefined

// Doesn't have to be uppercase, I just like uppercase constants ;)
const HARDLIMIT = 5;
HARDLIMIT = 20;
// Throws an error

Modules

You've probably encountered modules in JavaScript before, either with Node.js (require()) or with libraries such as CommonJS, AMD and others.

export const fpRatio = Math.PI * 9.23 / 0.7734;

export function facePalmFactor(embarrassmentLevel, handSize) {
  return embarrassmentLevel / handSize * fpRatio;
}
import facePalmFactor from 'lib/facePalm';
console.log(facePalmFactor(707, 618))

Maps and Sets

Two new types to play with in ES6 are Map and Set.

Map

A Map is similar to how a lot of developers currently use the Object type, as a key - value store. One of the advantages Map offers is that the key can be anything, whereas the key in an Object is always a string.

var map = new Map();
// Use a string a as a key
map.set('name', 'bob');

// 1 and '1' are not equal
map.set(1, 1).set("1", "One");
console.log(map.get(1), map.get("1"));
// Outputs: 1 "One"

// Use anything you like as a key
var obj = {some: 'random', data: Math.random()};
map.set(obj, {trash: 'rubbish'});
console.log(map.get(obj));
// Outputs: {trash: 'rubbish'}

console.log(map.size); // 4
Set

The Set type behaves as an ordered list, which will not contain any duplicates. You add items using its .add(value) method, which will check if it already contains that value using a === comparison (value and type). A Set doesn't allow random access, so you can't jump straight to the fifth item within one, but it is an ES6 iterator allowing you to walk through the values. It also provides a .has(value) method which returns a Boolean indicator of presence in the set.

var set = new Set();
s.add('I').add('like'.add('pi').add(3.14);
s.size; // 4

s.add(3.14);
s.size; // 4

s.add('3.14');
s.size; // 5

s.has('pi'); // true

WeakMap and WeakSet

Now that you've got an idea of Set and Map types, here are their weak variants. These work in almost the exact same way, except they don't hold onto references. If an object is garbage collected by your JavaScript engine, if it had a reference within a WeakMap or WeakSet, it'd still be collected and vanish from your WeakMap or WeakSet.

Another important point to remember is that these weak versions can not be iterated over. This is due to the fact that garbage collection may occur and change the data. If you need a list of the contents (such as the keys in WeakMap), you'd have to maintain a separate list.

Promise

Now a Promise is something that'll prove really useful when trying to deal with callbacks. It allows you to wrap code within a promise and use that promise to fulfil other code (or further promises). Error management is also easier with certain methods provided. These are worth investigating further, I'd really recommend research further if you haven't used them before.

// Famous setTimeout example
function fakeAsync(message) {
  return new Promise((resolve, reject) => {
    console.log(message);
    setTimeout(resolve, 923);
  });
}

fake('Hello')
  .then(() => {
    fakeAsync('Goodbye');
  }).then(() => {
    console.log('.');
  });


// --- fictional example
User.find({name: 'misterdai'})
  .then((user) => {
    return Post.save(data, user);
  })
  .catch((error) => {
    console.error(error);
    process.exit(1);
  });

Iterators, for-of and generators

I'll let the above resources explain these features. Simply because I haven't had chance to play around with them enough to confidently explain them and provide an example. Also, they feel worthy of their own blog post once I'm comfortable with them.

ES6 in Node.js

There isn't much in the way of support for ES6 in Node.js, but parts are there. This is mostly due to the specifications not being fully signed off yet and the V8 version Node.js uses being a little behind (also dependant on which Node version you use).

0.10.x (Stable)

At the time of writing this was the highest stable release line (0.10.33 to be exact). It doesn't contain much from ES6: Number.isFinite, Number.isNaN, const and some other minor parts. Even then, you may find the implementations incomplete or out of date (e.g. const is not block-scoped).

There are some command line options you can invoke to beef this support up a little, hidden in the list of V8 options (--v8-options).

--harmony_typeof (enable harmony semantics for typeof) type: bool default: false --harmony_scoping (enable harmony block scoping) type: bool default: false --harmony_modules (enable harmony modules (implies block scoping)) type: bool default: false --harmony_proxies (enable harmony proxies) type: bool default: false --harmony_collections (enable harmony collections (sets, maps, and weak maps)) type: bool default: false --harmony (enable all harmony features (except typeof)) type: bool default: false

Ignore --harmony-typeof, the reason why it isn't enabled when you use --harmony is due to it being rejected from the ES6 standard. Better off without it really. Other features like --harmony_modules won't work as the specification wasn't complete enough at the time.

Harmony 0.10

Running with the --harmony flag (e.g. node --harmony index.js) will brighten our dark little world a little with some ES6 goodies. But in places it'll work if you set strict mode "use strict"; in your files.

  • Better const support (strict only)
  • let support (strict only)
  • WeakMap's
  • block-level function declaration

Unstable 0.11

The 0.11 series has been around for over a year at the time of writing (release March 2013) and contains

  • Math.imul
  • Number complete number properties from ES6
  • Promise which makes async code management a little easier
  • WeakMap and WeakSet
Harmony 0.11

Take this version of node and running it with --harmony provides the following additional support for ES6, on top of what the harmony flag enables for 0.10.x.

  • All new addtions to Math.
  • Array now supports .find, .findIndex, .fill, .keys, .values, .entries.
  • Symbol functionality, although some areas are lacking.
  • String gets .repeat, .startsWith, .endsWith and .contains.
  • Map and Set
  • Octal and Binary literals and Number support.
  • Generators and the yield keyword.

Is this enough?

Not for me, what can I say apart from "I'm greedy". I could probably also say that I really like the look of some other ES6 functionality like: modules, classes and having more complete support of the features Node.js can supply.

Traceur

Enter Google's Traceur. This is a compiler that'll take your ES6 (and even some ES7) code and convert it into ES5 code, providing polyfills for missing features and rewriting code to behave as intended but in your ES5 environment (like Node.js or most web browsers).

You can use Traceur to run your code as is or pre-compile it before hand. I prefer to pre-compile it, as I may not want the extra overhead or additional files required when deploying the code.

Gluing it together with Grunt

This is where we'll actually start getting to work. The following example shows how I set up my project.

  1. mkdir es6
  2. cd es6
  3. npm init
  4. npm install --save-dev grunt grunt-traceur
  5. npm install -g grunt-cli

grunt-traceur

A grunt plugin that wraps traceur, making it a little easier to use. It makes it easier to set the options you may want to supply or even have different options for various parts of your source.

traceur vs. Node.js harmony

We've already established that Node.js has some ES6 support with it's harmony flag, especially the 0.11 series. In the following examples, I'm going to be targeting Node.js 0.11 with the --harmony flag enabled. Because of this choice, there are some ES6 features that I want traceur to leave alone. There's no point in recompiling code if Node.js can support it.

This may not be a complete list but I'll ask traceur to skip over Octal/Binary literals and generators.

Source maps!

If we're going to try and trace an error in our code back to our original source files, we'll need a map. Simply due to the fact that traceur will be altering our code to work in ES5. Traceur will support creation of the map files but we'll also need an additional module for Node.js to make use of them.

Enter source-map-support

npm install --save source-map-support

We won't actually make use of this module in any code under es6/src. Instead it'll go into the es6/index.js file which will bootstrap our code.

Traceur runtime

With all the code rewrites that Traceur performs, it also needs a runtime to inject the rest of its polyfills. Just like the source map support, we'll make use of this in our es6/index.js file.

npm install --save traceur-runtime

Structure

  • es6
    • lib - Destination for our compiled JavaScript (ES5)
    • src - Original source of our ES6 JavaScript
    • index.js - File used to bootstrap our code
    • Gruntfile.js - Our grunt file for compiling

All the code we create should live under es6/src, which is where we'll tell traceur to look.

Gruntfile.js

module.exports = function(grunt) {
  grunt.initConfig({
    traceur: {
      options: {
        sourceMaps: 'file',
        generators: 'parse',
        numericLiterals: 'parse',
        modules: 'commonjs'
      },
      custom: {
        files: [{
          expand: true,
          cwd: 'src/',
          src: ['**/*.js'],
          dest: 'lib/'
        }]
      },
    },
    }
  });

  grunt.loadNpmTasks('grunt-traceur');

  grunt.registerTask('default', ['traceur']);
};

Now we should be able to run grunt and any code contained within es6/src will be compiled by traceur and saved (along with source maps) to es6/lib.

index.js

Our main bootstrap file. This will be executed with node --harmony index.js and contains the following:

require('source-map-support').install();
require('traceur-runtime');

// Execute our code
require('./lib/index.js');

A minor example

Just to get you started, here is a rather short and daft example.

// es6/src/module.js
export const fpRatio = Math.PI * 9.23 / 0.7734;

export function facePalmFactor(embarrassmentLevel, handSize) {
  return embarrassmentLevel / handSize * fpRatio;
}
// es6/src/index.js
import {facePalmFactor} from './module.js';

console.log(facePalmFactor(10, 3));

Then run the following to compile and execute:

  1. grunt
  2. node --harmoney index.js

Final words

Good luck! If you have any problems putting this together, let me know and I'll try to help out. I'm still feeling my way around all of this myself, so if you've spotted a mistake or want to suggest something, feel free to let me know in the comments or the various other ways to contact me.


Comments


David Boyer
David Boyer

Full-stack web developer from Cardiff, Wales. With a love for JavaScript, especially from within Node.js.

More...