Michal Zalecki
Michal Zalecki
software development, testing, JavaScript,
TypeScript, Node.js, React, and other stuff

ECMAScript 6 Overview

ECMAScript 6 (also known as ECMAScript 2015) is the new, but not the newest (ECMAScript 7), version of ECMAScript standard and it's based on ECMAScript 5.1. Since August 2014, ES6 is feature frozen. After publication process which will start in March 2015, ES6 will be finished in June 2015. Despite that ES6 is not finished yet there aren't any good reasons to not use it today thanks to great tools like Traceur from Google or Babel. ES6 is solving many real-life problems which, as programmers, we are facing on a daily basis not only in browsers but also on the server side.

I'm going to give you an overview of some ECMAScript 6 features. This article will be updated accordingly and in parallel with the repository on GitHub. As the best way to learn new things is testing, the code in repository is shipped with Karma and Jasmine. It is possible to write ES6 and transpile it to ES5 thanks to Traceur Babel in this very case. Even though Babel is awesome tool it lacks of couple features e.g. Proxies (due to ES5 limitations). For better understanding which environment supports which features look at ECMAScript compatibility table.

Previously I was using Traceur, but after all I decided to migrate to Babel. Babel has better ES6 support in addition to that I encounter some minor configuration problems with Traceur. After Chrome update module unit tests have stopped passing. I consider Babel as a better and more reliable tool.

Take into consideration that the “final support” of your environment is the sum of compiler/polyfill and your browser or server. So, Traceur doesn't support WeakMap but Chrome supports it well and thats mean you can use WeakMap in your code. Chrome is used by default with the configuration from repository, if you are using it too don't forget about enabling experimental JavaScript flag.

You can also be interested in my presentation about ES6 features.

Let

The let keyword is similar to the good, old var. The var scope is enclosing function or global while let scope is the block, statement or expression. The new behavior prevents variables from leaking out of the scope which is common bug for developers who came from statically typed languages like Java.

expect(() => {
    if (true) {
        var x = 1;
    }
    expect(x).toEqual(1);
}).not.toThrowError();

expect(() => {
    if (true) {
        let x = 1;
    }
    expect(x).toEqual(1);
}).toThrowError("x is not defined");

Constants

Constants as you might expect cannot be overridden but also don't protect object properties. When it comes to scope, constants variables acts like let.

const x = 1;
const y = {x: 1};
const z = {x: 1};
x = 2;       // error
y = {x: 2};  // error
z.x = 2;
// overwriting fails
expect(x).toEqual(1);
expect(y).toEqual({x: 1});
// modifying works, properties are not protected
expect(z).toEqual({x: 2});

Arrow functions

Arrow functions are functions shorthand.

let square = x => x * x;

let triangleArea = (a, h) => a*h/2;

let triangleHeron = (a, b, c) => {
    let p = (a + b + c)/2;
    return Math.sqrt(p*(p-a)*(p-b)*(p-c));
};

let objectify = x => ({ value: x });

expect(square(13)).toEqual(169);
expect(triangleArea(4, 6)).toEqual(12);
expect(triangleHeron(3, 4, 5)).toEqual(6);
expect(objectify("foo")).toEqual({ value:"foo" });

If you are familiar with CoffeeScript the arrow functions works the same as functions created with fat arrow (=>). Except minimalistic syntax arrow functions unlike functions share the same this with enclosing them code (don't create a new scope).

let person = {
    name: "Bob",
    belongings: ["Car", "PC"],
    getProperties: function () {
        let properties = [];
        this.belongings.forEach(function (thing) {
            properties.push(this.name + " has " + thing);
        });
        return properties;
    },
    getProperties2: function () {
        let properties = [];
        // arrows share this with surrounding code
        this.belongings.forEach((thing) => {
            properties.push(this.name + " has " + thing);
        });
        return properties;
    }
};

expect(() => person.getProperties())
    .toThrow(new TypeError("Cannot read property 'name' of undefined"));
expect(person.getProperties2()).toEqual(["Bob has Car", "Bob has PC"]);

Default Parameters

Default parameters allow for assigning default value to a not specified arguments and let us leave parameter = parameter || default syntax behind.

function f(list, indexA = 0, indexB = list.length) {
    return [list, indexA, indexB];
}

expect(f([1, 2, 3])).toEqual([[1, 2, 3], 0, 3]);
expect(f([1, 2, 3], 1)).toEqual([[1, 2, 3], 1, 3]);
expect(f([1, 2, 3], 1, 2)).toEqual([[1, 2, 3], 1, 2]);

Classes

If you like it or not (like me), the ES6 class is only syntactic sugar over prototypes. In ES6 classes are very minimal and I don't see any special benefits of using them instead of constructor functions, but still, extends looks cleaner than Child.prototype = new Parent().

class Point {
    constructor(x = 0, y = 0) {
        this.x = x;
        this.y = y;
    }
    update(x = 0, y = 0) {
        this.x = x;
        this.y = y;
    }
}

class Circle extends Point {
    constructor(r, x, y) {
        super(x, y);
        this.r = r;
    }
    update(r, x, y) {
        super.update(x, y);
        this.r = r;
    }
    isPointIncluded(point) {
        if (point.constructor != Point)
            throw new Error("point must be an instance of Point");

        return Math.pow(this.r, 2)+Math.pow(this.y, 2) >=
               Math.pow(this.x-point.x, 2)+Math.pow(this.y-point.y, 2);
    }
}

let c1 = new Circle(3);
expect(c1.isPointIncluded(new Point())).toEqual(true);
expect(c1.isPointIncluded(new Point(0, 3))).toEqual(true);
expect(c1.isPointIncluded(new Point(3, 3))).toEqual(false);

let c2 = new Circle(6, 2, 1);
expect(c2.isPointIncluded(new Point(2, 7))).toEqual(true);
expect(c2.isPointIncluded(new Point(3, -1))).toEqual(true);
expect(c2.isPointIncluded(new Point(6, 6))).toEqual(false);
c2.update(6, 2, 2);
expect(c2.isPointIncluded(new Point(6, 6))).toEqual(true);

Destructuring Assignment

Destructuring Assignment lets us pull out specified values from array or object and save it to variables.

let [a, , [b, c]] = [1, 2, [3, 4]];
expect(a).toEqual(1);
expect(b).toEqual(3);
expect(c).toEqual(4);

let {firstName, lastName: surname, info: {age, driver}} =
    {firstName: "Foo", lastName: "Bar", info: {age: 20, driver: true}};

expect(firstName).toEqual("Foo");
expect(surname).toEqual("Bar");
expect(age).toEqual(20);
expect(driver).toEqual(true);

Rest Parameters

Vardic functions is a common pattern for many languages e.g. Ruby, PHP (5.6+). It allows functions to have multiple parameters as array. It results in giving up using arguments object.

function buy(where, ...items) {
    return "I'm going to " + where + " to buy "
        + items.length + " items: "
        + items.slice(0, -1).join(", ")
        + " and " + items.slice(-1) + ".";
}

expect(buy("the mall", "jacket", "bag", "sweets", "headphones"))
    .toEqual("I'm going to the mall to buy 4 items: "
           + "jacket, bag, sweets and headphones.");

Spread

Spread operator is an inversion of rest parameters. It splits up array into function parameters. Spread can be combined with default parameters as well.

function send(what, where, toWhom) {
    return "I'm sending " + what + " to " + toWhom
         + " who is in " + where + ".";
}

function send_with_default(what, where, toWhom = "Santa") {
    return "I'm sending " + what + " to " + toWhom
        + " who is in " + where + ".";
}

expect(send(...["the letter", "Poland", "Mike"]))
    .toEqual("I'm sending the letter to Mike who is in Poland.");
expect(send_with_default(...["the letter", "Lapland"]))
    .toEqual("I'm sending the letter to Santa who is in Lapland.");

Symbols

Symbols are new immutable and unique data type. Symbols are neither primitives nor objects, they have their own type. It can be used as identifier for object properties including WeakMap. Optional name can be used for debugging and by Symbol.for method. Symbol cannot be used as a constructor.

let s = Symbol("foo");
expect(s).not.toEqual(Symbol("foo"));
expect(typeof s).toEqual("symbol");

let s2 = Symbol.for("foo");
expect(s).not.toEqual(s2);
expect(s2).toEqual(Symbol.for("foo"));
expect(Symbol.keyFor(s2)).toEqual("foo");

expect(Symbol("bar")).not.toBe(Symbol("bar"));
expect(Symbol.for("bar")).toBe(Symbol.for("bar"));

It may seems that symbols are excellent for simulating private properties. Although symbols are unique they can by accessed.

function Safe(secretData) {
    let s = Symbol("secret symbol");
    this[s] = secretData;
}

let obj = new Safe("secret");

expect(obj["secret symbol"]).toBeUndefined();
expect(obj[Symbol("secret symbol")]).toBeUndefined();
expect(Object.getOwnPropertySymbols(obj)).toEqual(jasmine.any(Array));
expect(obj[Object.getOwnPropertySymbols(obj)[0]])
    .toEqual("secret");

There is also something called well-known symbols which can by used for awesome things e.g. iterators. We will talk about iterators in a moment.

Enhanced Object Literals

Enhanced Object Literals bring more flexibility when defining object properties. Object properties now can be computed, shorthand, methods or gives access to __proto__. Also making super calls should be possible but it isn't available using Traceur or Babel.

function greet(name) {
    return "Hello " + name;
}
let x = 2;
let obj = {
    // Computed property names
    [x*2]: "Computed Property Name",
    // __proto__
    __proto__: {
        hi: function () { return "Hi!" },
        by: function () { return "By!" }
    },
    // object initializer shorthand (greet: greet)
    greet,
    cheers(name) {
        return "Cheers " + name;
    }
    // @TODO making super calls
};

expect(obj[4]).toEqual("Computed Property Name");
expect(obj.hi()).toEqual("Hi!");
expect(obj.by()).toEqual("By!");
expect(obj.greet("Bob")).toEqual("Hello Bob");
expect(obj.cheers("Bob")).toEqual("Cheers Bob");

Iterators

Iterators are traversable by for...of loop objects. Iterators are decelerated with Symbol.iterator and should return the object with the next method. Iterators are similar to generators but instead of yielding a value they explicitly return object which contains two properties. The done indicates whether iteration is finished and value is just the return.

function fibonacci(i) {
    return {
        [Symbol.iterator]() {
            let pre = -1, cur = 1;
            return {
                next() {
                    [pre, cur] = [cur, pre + cur];
                    return {done: !(i--), value: cur};
                }
            }
        }
    }
}

let fib = [];
for (let n of fibonacci(10)) {
    fib.push(n);
}
expect(fib).toEqual([0, 1, 1, 2, 3, 5, 8, 13, 21, 34]);

Generators

(Not)Objectively generators are outstanding! They deserve for their own article(s) (just like Promises) but I promised you it will be just an overview. Generators are decelerated like normal functions but there must be a * after function word and generators are yielding values.

function* foo() {
    let i = 0;
    yield ++i;
    yield ++i;
    yield ++i;
}

let seq = foo();
expect(seq.next().value).toEqual(1);
expect(seq.next().value).toEqual(2);
expect(seq.next().value).toEqual(3);

Do you see similarities to iterators? There is also a yield* which tells us I'm yielding another generator!. I like it very much, but I'm not sure how to use it in real-life example. Let me now if you figure out something more practical than this one.

function* flatten(t, n = 0) {
    if (t[n]) {
        if (Array.isArray(t[n]))
            yield* flatten(t[n])
        else
            yield t[n];
        yield* flatten(t, n + 1);
    }
}

let nums = [];
for (let n of flatten([10, 11, 12, [13, 14, [15, 16]], 17])) {
    nums.push(n);
}
expect(nums).toEqual([10, 11, 12, 13, 14, 15, 16, 17]);

The method next actually accepts an argument which will be returned by yield.

// The idea comes from http://youtu.be/s-BwEk-Y4kg?t=14m42s
function* powGenerator() {
    return Math.pow(yield "a", yield "b");
}

let g = powGenerator();
expect(g.next().value).toEqual("a");
expect(g.next(10).value).toEqual("b");
expect(g.next(2).value).toEqual(100);

How awesome is that!

If you would like to now more about generators check out Kyle Simpson's series about ES6 generators.

Proxies

Proxy can teach an old object new tricks. Proxies changes the fundamentals of how the object works as a result of which it's almost impossible to write a polyfill to simulate proxy mechanism. Check the native Proxy support. Proxies make the code a lot more cleaner. Just simple assignments instead of custom setters.

var address = {
    'Marie Lynch': '[email protected]',
    'Ryan Bradley': '[email protected]' };

var handler = {
    set: (target, property, value, receiver) => {
        if (!value.match(/^\S+@\S+\.\S+$/))
            throw new TypeError(`${value} is invalid email!`);
        target[property] = value;
        return true;
    },
    get: (target, property, receiver) => {
        return property in target ?
            target[property] : "Not Found"; }
};

var addressBook = new Proxy(address, handler);

addressBook['Joseph Fields'] = '[email protected]';

expect(() => { addressBook['Kathryn Lewis'] = 'klewis.com' }).
    toThrow(new TypeError("klewis.com is invalid email!"));
expect(addressBook['Marie Lynch']).toEqual("[email protected]");
expect(addressBook['Joseph Fields']).toEqual("[email protected]");
expect(addressBook['Kathryn Lewis']).toEqual("Not Found");

Set and get aren't the only traps. Check out the repo for more examples and Proxy at MDN for full list of available traps.

Numeric Literals

Numeric literals make working with binary and octal numbers a piece of cake.

expect([
    0b111,
    0b11110000,
    0b00001111
]).toEqual([
    7,
    240,
    15
]);

expect([
    0o7,
    0o360,
    0o17
]).toEqual([
    7,
    240,
    15
]);

Template literals

Template literals allow for using variables in stings avoiding awful var1 + “ ” + var2 for something so simple like putting space between them. It's available for a long time in different languages like Ruby or PHP. It's called string interpolation.

let name = "Foo";
let surname = "Bar";
let email = "[email protected]";

expect(`${name} ${surname}`).toEqual("Foo Bar");

What is more, template strings can be tagged. This more advanced form of templates let us modifying an output through tag function.

let name = "Foo";
let surname = "Bar";
let email = "[email protected]";

function vCard(strs, ...values) {
    let card = {};
    let regExp = /[\t ]*([a-zA-Z@\. ]+): /;
    for (let str of strs) {
        if (regExp.test(str)){
            card[str.match(regExp)[1]] = values.shift();
        }
    }
    return card;
}

expect(
    vCard`First name: ${name}
    Last name: ${surname}
    Email: ${email}`
).toEqual({
    "First name": "Foo",
    "Last name": "Bar",
    Email: "[email protected]"
});

Promises

Promises are a huge step forward in JavaScript. Promises seams natural when it comes to asynchronous tasks especially when the one task depends on the result of another. We known how to react to some specified even, but we don't now when this event will be fired. There is a lot of implementations of this pattern like $q in AngularJS which is based on Kris Kowal's Q or rsvp.js.

function promiseMaker(condition, timeout = 2000) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            if (condition) {
                resolve("Success!");
            } else {
                reject(new Error("Something went wrong!"));
            }
        }, timeout);
    });
}

promiseMaker(true)
    .then((data) => {
        expect(data).toEqual("Success!");
    });

promiseMaker(false, 3000)
    .catch((err) => {
        expect(err).toEqual(new Error("Something went wrong!"));
    });

If you are looking for more examples check out promices.js from the repository.

Modules

Modules allow for convenient using dependencies thanks to asynchronous loading and explicit exports. We can then load whole module or import specific values.

// modules/math.js
export function sum(x, y) {
    return x + y;
}
export const pi = 3.141593;

// modules/person.js
export let name     = "Foo";
export let surname  = "Bar";

// modules/awesome.js
export default class {
    constructor() {
        this._status = "awesome";
    }
    hoItIs() {
        return this._status;
    }
}

// modules.js
import {sum, pi} from "./modules/math";
import * as buddy from "./modules/person";
import Awesome from "./modules/awesome";

expect(sum(2, 3)).toEqual(5);
expect(pi).toEqual(3.141593);

pi = 22/7; // error: "pi" is read-only

expect(buddy).toEqual(jasmine.any(Object));
expect(buddy.name).toEqual("Foo");
expect(buddy.surname).toEqual("Bar");

expect((new Awesome).hoItIs()).toEqual("awesome");

Modules loading works both on the server side and in the browser.

<script>
    System.import('modules/math').then((m) => {
        expect("2π = " + m.sum(m.pi, m.pi)).toEqual("2π = 6.283186");
    });
    System.import('modules/person').then((m) => {
        expect("I'm " + m.name + " " + m.surname).toEqual("I'm Foo Bar");
    });
</script>

If you are interested in modules Webpack, RequireJS, Browserify and jspm.io are worth looking.

Map, Set, WeakMap and WeakSet

Map, Set, WeakMap and WeakSet are new data structure. Well known from e.g. Java and quite similar to each other but, as usual, the devil is in the detail.

Map is a simple key/value map. Objects, symbols and primitives (any value) can be used as keys.

let m = new Map([["name", "Foo"], ["surname","Bar"]]);
m.set("age", 10).set("age", 20).set(false, "Foo");

expect(m.size).toEqual(4);

expect(m.has("name")).toEqual(true);
expect(m.has(false)).toEqual(true);
expect(m.has("address")).toEqual(false);

let mapIter = m.entries();

expect(mapIter.next().value).toEqual(["name", "Foo"]);
expect(mapIter.next().value).toEqual(["surname", "Bar"]);
expect(mapIter.next().value).toEqual(["age", 20]);
expect(mapIter.next().value).toEqual([false, "Foo"]);
expect(mapIter.next().value).toBeUndefined();

expect(m.delete("name")).toEqual(true);
expect(m.has("name")).toEqual(false);
expect(m.size).toEqual(3);

m.clear();
expect(m.size).toEqual(0);

Set stores unique values of any type.

let s = new Set(["Foo", "Bar"]);
s.add(false).add(123).add("Bar");

expect(s.size).toEqual(4);

expect(s.has("Bar")).toBe(true);
expect(s.has(123)).toBe(true);
expect(s.has(true)).toBe(false);

let setIter2 = s.values();

expect(setIter2.next().value).toEqual("Foo");
expect(setIter2.next().value).toEqual("Bar");
expect(setIter2.next().value).toEqual(false);
expect(setIter2.next().value).toEqual(123);
expect(setIter2.next().value).toBeUndefined();

expect(s.delete("Bar")).toEqual(true);
expect(s.has("Bar")).toEqual(false);
expect(s.size).toEqual(3);

s.clear();
expect(s.size).toEqual(0);

WeakMap is a collection of key/value pairs. Keys are objects and values are arbitrary type. Is like Map at first glance. The difference is WeakMap keys must be an object and aren't enumerable.

let wm = new WeakMap(),
    o1 = {},
    o2 = function () {},
    o3 = Symbol("foo"),
    o4 = window;

wm.set(o1, 123);
wm.set(o2, "FooBar");
wm.set(o3, undefined);
wm.set(1, "Baz"); // Invalid value used as weak map key

expect(wm.get(o1)).toEqual(123);
expect(wm.get(o2)).toEqual("FooBar");
expect(wm.get(o3)).toBeUndefined();
expect(wm.get(o4)).toBeUndefined();

expect(wm.has(o1)).toEqual(true);
expect(wm.has(o2)).toEqual(true);
expect(wm.has(o3)).toEqual(true);
expect(wm.has(o4)).toEqual(false);

wm.delete(o1);
expect(wm.has(o1)).toEqual(false);

WeakSet is the the simplest data structure of all. It stores store weakly held objects.

let ws = new WeakSet(),
    o1 = {},
    o2 = function () {},
    o3 = window;

ws.add(o1);
ws.add(o2);

expect(ws.has(o1)).toEqual(true);
expect(ws.has(o2)).toEqual(true);
expect(ws.has(o3)).toEqual(false);

ws.delete(o1);
ws.delete(o2);

expect(ws.has(o1)).toEqual(false);
expect(ws.has(o2)).toEqual(false);

Presentation

If you are interested check out also my presentation about ES6 which I gave on Bydgoszcz Web Development Meetup.

Hero image by Roberta Sorge on Unsplash.