ECMAScript 6:
The Next GeneratioN
Presenter: Steve Venzerul
What is ES6 and why should I care?
ES6 is the new and upcoming specification for the Javascript language. It defines a fairly large set of new builtin language constructs and a set of runtime APIs to be implemented by both browsers and engines like NodeJS.
The reason you should care is that many of the proposed features significantly reduce development time and alleviate the pain of implementing certain common and not so common features.
Some definitions
1. TC39 Commitee - Standardization of the general purpose, cross platform, vendor-neutral programming language ECMAScript. This includes the language syntax, semantics, and libraries and complementary technologies that support the language.
2. ECMAScript.next is the code name of the next version of ECMAScript. Using that term implies that one is discussing features that may or may not be added to the final standard.
3. ECMAScript 6 is the actual (final) name of ECMAScript.next. Using that term implies that one is talking about features that will definitely be in the standard.
4. ECMAScript Harmony is a superset of ECMAScript.next and means “features coming up after ECMAScript 5”. According to Eich, it comprises ECMAScript 6 and ECMAScript 7.
Array Comprehensions
//Replacing map(). Maps to Cube root using the ES6 function Math.cbrt().
> [for (i of [1,2,3,4] Math.cbrt(i)];
//Outputs:
Array [ 1, 1.2599210498948732, 1.4422495703074083, 1.5874010519681996 ]
//Replacing filter(). Filters out odd numbers.
[for (i of [2,3,4,5,7,8,9,12,13]) if (!(i % 2)) i];
//Outputs:
Array [ 2, 4, 8, 12 ]
Array compehernsions, which some of you may know from Python and other functional languages, are a syntactic sugar for building lists and arrays. They're generally make use cases like .map() and .filter() a little easier and less repetitive.
Destructuring
Yet another syntactic sugar feature, that some of you may recognize from Python, destructuring sugars assigning multiple values to variables, most often breaking up an array intro it's elements. The syntax is a little funky and not quite as clean as Python, but its certainly step forward in minimizing verbosity.
let [x, y] = ['hello', 'friend'];
console.log(x);
//hello
console.log(y);
//friend
//More interesting example
let [namespace, action] = 'messages:edit'.split(':');
console.log(namespace, action);
//"messages" "edit"
//skipping elements
let [ , month, day] = /^(\d\d\d\d)-(\d\d)-(\d\d)$/.exec('2014-09-08');
console.log(month, day);
//09 08
//Object destructuring
let { first, last } = {
first: 'John',
last: 'Smith'
};
console.log(first, last);
//"John" "Smith"
Default Parameter Values
Default parameters are here to alleviate the pain of having to assign defaults at the beginning of every function that requires defaults for it's arguments. So instead of writing this:
function someFn(param1, param2, param3) {
param1 = param1 || 'defaultValue1';
param2 = param2 || 'defaultValue2';
param3 = param3 || 'defaultValue3';
}
We can instead do this:
function fn(param1 = 'defaultValue1', param2 = 'defaultValue2', param3 = 'defaultValue3') {
console.log(param1, param2, param3);
}
Default values may be any primitives, as well as objects. Pretty much anything value that can be computed at parse time can be used as a default parameter which makes this feature quite powerful.
You can skip values and have defaults applied as follows:
fn('str1', undefined, 'str2');
//str, defaultValue2, str2
Fat Arrow Notation
Fat arrow notation is syntactic sugar for writing named and anonymous functions. It's particularly suited for the latter case due to the prevalent style in JS of passing around functions as callbacks. More importantly, unlike regular functions, they support a lexical 'this', which simply means they retain a correct reference to the 'this' variable of the surrounding code. Let's look at some examples:
//Anonymous arrow notation function
let f = x => x + 1
//Multiple arguments
let ma = (x, y) => x * y
//Block body
let fb = (x, y) { return Math.pow(x, y) }
//Arrow functions can only be named via variable assignment
let subtract = (x ,y) => x - y
Caveats
1. Arrow functions have a lexical 'this'
let basket = {
basketName: 'myBasket',
contents: ['oranges', 'bread', 'salami', 'cheeze'],
printBasket: function() {
this.contents.forEach((item) => {
console.log(this.basketName + ' contains ' + item);
});
}
}
2. Always have a bound 'this', so calling arrow functions with .call() or .apply() cannot change the value of 'this'. You can, however use them to pass arbitrary amounts of aruments.
3. They don't have a [[Construct]] internal method, nor a prototype property. This means they cannot be invoked as a constructor so the follwing code will fail:
new (() => {})
Caveats (cont')
4. They don't have the 'arugments' special variable. They achieve the same functionality with the more syntactical mechanisms of Rest, Spread, and default values.
//y will be an array of arguments
function fn(x, ...y) {
console.log(y.length);
}
5. Named arrow functions are under discussion and may not be implemented in the ES6 spec.
Classes
As anyone who's ever had to implement classes in JS knows, they're highly vebose and cumbersome, forcing us to use the dreaded prototype syntax, as well as manually fixing the constructor
function className() {}
className.prototype.methodName = function() {}
className.prototype.constructor = className;
Not only is this syntax not semantic, it's quite verbose and feels like a hack, something dressed onto the language as an afterthought rather than a first class feature. And to top it all off, there's no notion of 'super' which leads to fun things like
Backbone.View.prototype.render.call(this);
ES6 classes propose solutions to almost all of these issues.
A Simple Class
class Monster extends Character {
// A method named "constructor" defines the class’s constructor function.
constructor(name, health) {
this.name = name;
this._health = health;
//Perform superclass initialization
super();
}
// An identifier followed by an argument list and body defines a
// method. A “method” here is simply a function property on some
// object.
attack(target) {
console.log('The monster attacks ' + target);
}
// The contextual keyword "get" followed by an identifier and
// a curly body defines a getter in the same way that "get"
// defines one in an object literal.
get isAlive() {
return this._health > 0;
}
// Likewise, "set" can be used to define setters.
set health(value) {
if (value < 0) {
throw new Error('Health must be non-negative.')
}
this._health = value
}
}
Usage
// The only way to create prototype data properties is by
// modifying the prototype outside of the declaration.
Monster.prototype.numAttacks = 0;
// Immutable properties can be added with defineProperty.
Object.defineProperty(
Monster.prototype,
"attackMessage",
{ value: 'The monster hits you!' }
);
let monster = new Monster('Ogre', 100);
monster.attack('poor schmuck');
console.log('The monster is ' + (monster.isAlive ? 'alive' : 'dead') + '!');
console.log('The monster attacked you ' + monster.numAttacks + ' times');
//Will output:
> The monster attacks poor schmuck
> The monster is alive!
> The monster attacked you 0 times
Promises
We've had promises around for some time now in the form of libraries like Q and When.js, but ES6 finally introduces this concept as a native object. Promises generally resolve the issue of large callback hell style problems like the following:
fs.readdir(source, function(err, files) {
if (err) {
console.log('Error finding files: ' + err)
} else {
files.forEach(function(filename, fileIndex) {
console.log(filename)
gm(source + filename).size(function(err, values) {
if (err) {
console.log('Error identifying file size: ' + err)
} else {
console.log(filename + ' : ' + values)
aspect = (values.width / values.height)
widths.forEach(function(width, widthIndex) {
height = Math.round(width / aspect)
console.log('resizing ' + filename + 'to ' + height + 'x' + height)
this.resize(width, height).write(destination + 'w' + width + '_' + filename, function(err) {
if (err) console.log('Error writing file: ' + err)
})
}.bind(this))
}
})
})
}
})
Instead, promises let us do awesome things like this:
getSomeData()
.then(filterTheData)
.then(processTheData)
.then(displayTheData);
The ES6 promise constructor and basic usage
function timeout(duration = 0) {
return new Promise((resolve, reject) => {
setTimeout(resolve, duration);
})
}
var p = timeout(1000).then(() => {
return timeout(2000);
}).then(() => {
throw new Error("hmm");
}).catch(err => {
return Promise.all([timeout(100), timeout(200)]);
})
Side note: jQuery has a concept of deferred which is somewhat like a promise but does not properly conform the A+ spec. However, it has a then method which means it can be converted to proper ES6 promise. Be carefull of jQuery passing multiple arguments to resolve and reject.
String and Array APIs
//Notable String APIs
'na '.repeat(8) + 'batman!';
//na na na na na na na na batman!
('na '.repeat(8) + 'batman!').contains('batman')
//true
'going to the store'.startsWith('going');
//true
'going to the store'.endsWith('batman');
//false
//************************************************//
//Notable Array APIs
[1,2,3,4,5].find(function(el, index, list) { if(el === 1) return true; });
//1
Array(10).fill('a');
//["a", "a", "a", "a", "a", "a", "a", "a", "a", "a"]
['a', 'b', 'c', 'd', 'e'].keys().next();
//{value: 0, done: false}
['a', 'b', 'c', 'd', 'e'].values().next();
//{value: 'a', done: false}
['a', 'b', 'c', 'd', 'e'].entries().next();
//{value: [0, 'a'], done: false}
Array.of({}, 'a', 123, true);
//[{}, 'a', 123, true]
Block Scope, Let and Constants
To fix Javascript's confusing scope and hoisting rules, ES6 introduces block scope as well as the let and const keywords.
function variableHoist() {
console.log(hoisty);
hoisty = 1;
console.log(hoisty);
var hoisty = 2;
console.log(hoisty);
}
variableHoist();
//outputs undefined (would get a ReferenceError if no var declaration existed in scope)
//outputs "1"
//outputs "2"
The let keyword effectively replaces var, and const is a new keyword which allows for truely frozen constants enforced by the runtime. 'use strict' helps with some of this, but is not as elegant.
Example of Block Scoping
function f() {
{
let x;
{
//block scoped constant
const x = "sneaky";
// error, assignment to constant
x = "foo";
}
// error, already declared in block
let x = "inner";
}
}
Generators
Generators are one of the trickier parts of the spec, but if you're familiar with Python, the yield keyword should ring a bell. A very simple generator example:
function* gen(i){
while(true){
yield i++;
}
}
Although innocuous looking, this simple function will generate an infinite stream of numbers, and it will do so lazily and without overrunning memory. An interesting side effect of this behaviour is that one must return control to the generator function once it yielded, as it cannot resume itself.
Beware of return statements in generators, if using a for...of loop to consume them, the return statement will be ignored.
function *foo() {
yield 1;
return 2;
}
Proxies
Proxies enable proper runtime level support for interception, virtualization, logging and profiling. Many test frameworks like (mocha, jasmine, etc.) perform various hacks and work arounds to instrument objects for inspection and mocking purposes. A large chunk of their hacks can now be easily replaced using the new Proxy object. Other uses include live instrumentation of objects while minimally distrupting the object's original functionality and allowing for hooked method logging and profiling.
let obj = {
a: 'str',
b: 123
};
let p = new Proxy(obj, {
get: (reciever, propName) => {
console.log(typeof reciever[propName])
}
});
p.a
//"string"
p.b
//"number"
There's more
let handler = {
get: function() {},
set: function() {},
has: function() {},
deleteProperty: function() {},
apply: function() {},
construct: function() {},
getOwnPropertyDescriptor: function() {},
defineProperty: function() {},
getPrototypeOf: function() {},
setPrototypeOf: function() {},
enumerate: function() {},
ownKeys: function() {},
preventExtensions: function() {},
isExtensible: function() {}
}
Proxies can handle virtually all of the built in terms of interception.
Sets and Maps
Javascript is finally getting a proper implementation of maps and sets in the form of the native objects Map() and Set(). The Set is an array-like object which will ensure that all values are unique, making deduping of arrays unnecessary. The Map object, is somewhat similar in functionality to regular Objects, however, keys need not be strings, they may any primitive or even another object.
// Sets
var s = new Set();
s.add("value1").add("value2").add("value1");
s.size === 2;
s.has("value1") === true;
// Maps
var m = new Map();
m.set("key", "value");
m.set(s, 34);
m.get(s) == 34;
//Maps and sets implement the iterator protocol so this is valid
let o = {a: 'b'},
m = new Map();
m.set(o, '123');
for (v of m) {
console.log(v);
}
//[ {a: 'b'}, '123' ]
Modules
A long awaited feature, modules are finally here to start solving the problem of encapsulation and modularization. As mentioned in a previous talk, the existence of things like Browserify and RequireJS is indicative of a missing feature in the language itself. Virtually every other non-trivial language has a concept of a module, and finally, Javascript is catching up.
// 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
//Use destructuring to import square and diag
import { square, diag } from 'lib';
console.log(square(11)); // 121
console.log(diag(4, 3)); // 5
ES7: Object.observe()
Technically speaking, Object.observe is not part of the ES6 spec, however, Google has been making a serious push towards the implementation of this features, even to the point the latest Chrome 37 already has a working .observe() function.
Every self-respecting MVC framework has a notion of an object which can have another object susbcribe to or observe the events that it emits. Most frameworks go the route of .get() and .set() methods, and simply trigger events manually when changes occur, Angluar being the exception in it's use of dirty checking.
Both approaches have flaws, get/set being unsemantic, adding overhead, and Angular having to jump through some serious hoops and face performance problems with it's dirty checking method.
Object.observe() addresses all of these issues.
Observing Objects
var model = {
prop1: 'str',
prop2: 123
};
function observerFn(changes) {
// Do something with changes
changes.forEach(function(propChange) {
console.log('Name of property: ', propChange.name);
console.log('Type of change: ', propChange.type);
if (propChange.type === 'update')
console.log('The old value was: ', propChange.oldValue);
});
}
Object.observe(model, observerFn);
> model.prop1 = 'new string';
//Name of property: prop1
//Type of change: update
//The old value was: str
> delete model.prop2
//Name of property: prop2
//Type of change: delete
Object.unobserve(model, observerFn);
//Observing only specific events
Object.observe(model, observerFn, ['delete', 'update']);
var arr = [1,2,3,4,5,6];
function observerFn(changes) {
console.log(JSON.stringify(changes, null, 2));
}
Array.observe(arr, observerFn);
arr.push(7);
// [
// {
// "type": "splice",
// "object": [ 1, 2, 3, 4, 5, 6, 7 ],
// "index": 6,
// "removed": [],
// "addedCount": 1
// }
// ]
arr[1] = 18;
// [
// {
// "type": "update",
// "object": [ 1, 18, 3, 4, 5, 6, 7 ],
// "name": "1",
// "oldValue": 2
// }
// ]
delete arr[2];
// [
// {
// "type": "delete",
// "object": [ 1, 18, null, 4, 5, 6, 7 ],
// "name": "2",
// "oldValue": 3
// }
// ]
Using ES6 Today
1. Traceur - Currently contains the most complete support of the ES6 spec, and provides for the best playground to try out new features and get a feel for the new syntax. Easily installed by cloning the repo and running 'make'.
2. Chrome Flag 'Enable Experimental Javascript' - You can enable all support ES6 features in Chrome by visiting chrome://flags and setting that option to 'enabled'. Chrome's support is not bad, but Traceur and Firefox are more complete in their current implementations.
3. Firefox - Doesn't require any special flags, pretty much everything is enabled by default in the latest firefox. Except for Traceur, it has the most complete ES6 implementation, and is probably the easiest to play around with since many people will already have it installed.
4. NodeJS --harmony flag - Node, being a wrapper around google's V8, support a set of flags starting with --harmony-{flagName} to enable either individual ES6 features, or all of them via the --harmony flag. To obtain a list of all flags from the latest Node (0.11.13) run:
node --v8-options | grep harmony
Resources
- A gist by Addy Osmani providing a massive list of tools that can help you start coding in ES6 today. Includes transpilers, compilers, build tasks and other goodies. ES6 Tools
- Axel Rauschmayer's Blog - A veritable treasure trove of tips, explanations and advice on all things JS. He has an extensive section on ES6 and upcoming features.
- ES6 Features - A pretty nice gist with examples and a bit of details on lots of upcoming features including those discussed in this talk.
- ES6 Support Table - A very extensive feature matric detailing how much of the ES6 Spec a variety of browsers and environments already implments. Careful though, sometimes they get it wrong.
- ES6 Draft Spec - Kinda dry, but if you're looking for nitty gritty, accurate algorithmic details of feature implementation, this is the place to find it.
ECMAScript 6: The Next Generation
By signupskm
ECMAScript 6: The Next Generation
- 1,636