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 ES6Promise
which makes async code management a little easierWeakMap
andWeakSet
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
andSet
- 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.
- mkdir es6
- cd es6
- npm init
- npm install --save-dev grunt grunt-traceur
- 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:
- grunt
- 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.