Finally catchup on ES6

A sprint through the newer bits of javascript

Overview

I have managed to avoid most of the gnarly details of javascript beyond ES5 for many years. Unfortunately a side project has driven me to finally take it on board. These are my notes, gleaned mostly by reading ES6 In Depth from Jason Orendorff of Mozilla.

I now know enough to be more dangerous.

Iterators

The of loop operator is like in but you don't need to filter out elements with hasOwnProperty

for (var value of myCollection) {
  console.log(value);
}

myCollection could be an array [], "a string", Map's andSet's (or any generator, see next section). To iterate over an object's keys you'd have to use Object.keys()

Generators

function* someGenerator(any, args) {
  yield "any type of return value" + any;
  console.log(args);
  yield "as many as you like";
}

Keywords here are function* specifically the * suffix ( this is a generator) and yield (generate a return a value). Additionally it is possible to yield all the values of another generator using yield*.

function* anotherGenerator(any, args) {
  // all of these are returned
  yield* someGenerator(any, args);
  // and then this one
  yield "as many as you like";
}

The actual return type of a generator function is Object.Generator. Thought you can skip direct use of this type by using the for of loops.

var g = someGenerator();
var item = g.next();
// possible values for item
// { value: some_value, done: false }
// { value: undefined, done: true }

When done boolean is false there is more data, The value is whatever yield returned from the generator.

Generators run in the callers thread. They can get more complicated but frankly until I've used them in the wild I can live without that.

String interpolation

Back-ticks can be used to quote strings (with line breaks too),

var aString = `any text
with line breaks`;

but more importantly and contain variables (and expressions) to expand and can span lines (note the ${...}),

var u = {name:"Adam",isAdmin:false};
console.log(`User: ${u.name} ${u.isAdmin?"admin":"not-admin"}`);
// User: Adam not-admin

Escape with '\' for both ‘$’ and ‘`’.

Tagged template literals

Prefix a backtick string with a tag. The tag is a function pointer that is designed to operate during string expansion.

function myTagger(template, ...args) {
    var result = "";
    for (var i = 0; i < args.length; i++) {
        if (i < template.length) {
            result += template[i];
        }
        result += "*" + args[i] + "*";
    }
    return result;
}
var v1 = 2, v2 = "four", v3 = "last";

console.log(myTagger`one ${v1} three ${v2} ${v3}`);
// one *2* three *four* *last*

...args is variadic, explained in the next section (but it does what you expect).

myTagger could make the arguments uppercase, or force a particular date or decimal layout.

More details here.

Variadic and default arguments

Variadic (aka rest) arguments

Works the way you want them to, prefix the last argument with ... indexed from the variadic parameter not from the first argument. This works with arrow functions too, unlike the built in arguments variable that works with functions but not arrow functions.

function fn(one, two, ...theRest){
    console.log(`one=${one}, two=${two}`)
    for (var i = 0; i < theRest.length; i++) {
        console.log(`theRest[${i}]=${theRest[i]}`)
    }
}

fn(1,2,"three","four",5);
// one=1, two=2
// theRest[0]=three
// theRest[1]=four
// theRest[2]=5

The opposite of rest parameters is spread, which expands any iterable value into separate parameters.

var f = (p1, ...pr) => {console.log(p1, pr);};
var a = [1, 2, 3];
f(99, a);    // p1===99, pr[0]===a
f(...a);     // p1===1, p[0]===2, p[1]===3

Also, with arrays

var [head, ...tail] = [1, 2, 3, 4];
var reJoined = [head, ...tail];

Defaults for function arguments

Important differences with javascript defaults compared to C# or Java:

  • Can be anywhere in the argument list
  • Defaults are evaluated at call time
  • Arguments are evaluated left to right (see arg3 and arg4 below)
var fnX() => "some-result";
function fn (
        arg1 = "some-default",
        arg2,
        arg3 = (arg2 ? "arg1 is set" : "arg1 is not set"),
        arg4 = fnX()
    ) {
    console.log(`${arg1} | ${arg2} | ${arg3} | ${arg4}`);
}

fn();
// some-default | undefined | arg2 is not set | some-result
fn("v1");
// v1 | undefined | arg1 is not set | some-result
fn(undefined, "replacement");
// some-default | replacement | arg1 is set | some-result
fn(1, 2, 3, 4);
// 1 | 2 | 3 | 4

Destructuring

This feature allows extracting one or values from an object or array.

Destructuring Arrays

// pull the values from an array to separate variables
var z = [10, -99, 20, 30];
// note the blank skips the value '-99
var [a, /*this is a blank space*/ , b, c, d] = z;
// note d is undefined
console.log(`a=${a} b=${b}, c=${c}, d=${d}`);
// a=10 b=20, c=30, d=undefined

var [first, ...theRest] = z;
console.log(`first=${first}, theRest=${theRest}`);
// first=10, theRest=-99,20,30

Also for swapping variables,

var a = 69;
var b = 42;

console.log(a, b);
[a, b] = [b, a];
console.log(a, b);

Destructuring Objects

var customer = {name:"Adam", job:"typist", age: 21};
// new variable different name for attribute
var {name: n} = customer;
// local variable with same name as object
// pull more than one property
var {job, age, missing, use_default_if_missing = 99} = customer;

console.log(n, job, age, missing, use_default_if_missing);
// Adam typist 21 undefined 99

Push this idea further and use for function arguments and multiple function returns.

var fn = ({url, someOption = true}) => {
    console.log(`url:${url}, option:${someOption}`);
    return [10, "ok"];
};
var args = {url : "a-url"};
var [a, b] = fn(args);
// url: a-url, option: true

console.log(a, b);
// 10, ok

Given destructuring we can return multiple values from a function and de-structure them to single values either through an object or array

Destructuring Maps

Given the new type, Map and the iterator with of we can do this,

var map = new Map();
map.set("key1", "value1");
map.set(42, "value2");

for (var [key] of map) {
  console.log("only keys", key);
}
// only keys key1
// only keys 42

for (var [, value] of map) {
  console.log("only values", value);
}
// only values value1
// only values value2

Arrow functions

Work as expected, and as described above. However,

  • this behaves differently with arrow functions
  • arguments magic variable is not available in arrow functions

function literals

var obj = {
    value: 10,
    fnES5 : function(x) { return this.value; },
    fnES6(x) { return this.value; },
    // arrow function has no 'this'
    fnES6arrow: x => this.value
};
console.log(
    obj.fnES5(),
    obj.fnES6(),
    obj.fnES6arrow()
);
// 10 10 undefined

Note the syntax (above) for fnES6 declaration.

Symbols

Symbols are a new feature, partly to prevent new features breaking old websites.

In the snippet beJavascript allow attribute access via dot notation or via index [], So given the literal string name for the attribute it is accessible

var x = {};
x.enabled = true;
x["enabled"] = true;

The new Symbol() allows creating a key to index the property on that will never collide with anything else as the key is not a string, so two separate libraries could, for example, add the same logically named attribute to the global window object without any issues.

var enabled = Symbol("is enabled");
// add attribute value
window[enabled] = true;
// view the value
console.log( (window[enabled] );
// true

The argument is enabled is a description (for debugging mainly). Interesting attributes of Symbol

console.log( Symbol() === Symbol() ); // false
console.log(
    Symbol("description") === Symbol("description")
); // false

console.log( typeof Symbol() ); // symbol
// get a shared symbol
console.log(
    Symbol.for("some shared key") === Symbol.for("some shared key")
); // true

// There are 17 (as of writing) symbols that are 'well known' and build in
console.table(Reflect.ownKeys(Symbol));
// "length", "name", "prototype", "for", "keyFor",
// "asyncIterator", "hasInstance", "isConcatSpreadable",
// "iterator", "match", "replace", "search", "species",
// "split", "toPrimitive", "toStringTag", "unscopables"

Collections

There are 4 interesting collections

  1. Map
  2. Set
  3. WeakMap
  4. WeakSet

Map

A Map is a better version of making a hash from {}. Better because there is clear separation between key/value pairs and methods on the hash table. It is also more explicit, with members .set(), .get(),.has(), .keys(), etc. It is also iterable.

Note that not every type can successfully be used as a key, or more precisely objects that have value equality do not have always hash equality, resulting in duplicates.

Set

A Set works as expected, being a unique set of elements with no values, jus tthe keys. One thing to note is the constructor that takes a parameter is added by iteration.

let s1 = new Set("adam"); // length 3 [a, d, m]
let s2 = new Set(["adam"]); // length 1 ["adam"]

Weak collections

They work as their non-weak other half but miss some members and iteration. Specifically designed to prevent your code hanging on to elements that could otherwise be garbage collected.

Proxies

Proxies are complicated but give you the opportunity to hook into all manner of operations on an object from get/set of an attribute value to function calls and adding/removing a property.

A basic example:

var person = {
    _n: "",
    // get/set functions explained later
    get name() {return this._n;},
    set name(value) {this._n = value}
};
function loggingProxy(target) {
    let handler = {
        get: function (target, key) {
            console.log(`PROXY: getting ${key}!`);
            return target[key];
        },
        set: function (target, key, value) {
            console.log(`PROXY: setting ${key}!`);
            return target[key] = value;
        }
    };
    return new Proxy(person, handler);
}

var p = loggingProxy(person);
p.name = "adam";
// PROXY: setting name!
console.log(p.name);
// PROXY: getting name!
// adam

Classes

To make something like classes in OO languages we now have,

class B1 {
    message = "Hello";
    viewMessage(msg) {
        return `${this.message} ${msg}`;
    };
}
class C1 extends B1{
    F1 = 1;
    static S1 = 0;
    constructor(p1, p2, p3) {
        super();
        this.F1 = p1;
        this.F2 = p2;
        C1.S1++;
    };
    get aField() { return this.F1 };
    set aField(f) {
        // some validation
        this.F1 = f;
    };
    // logically static Count
    count() {return C1.S1};
    viewMessage(msg) {
        return "# " + super.viewMessage(msg.toUpperCase());
    };
}

var c = new C1();
c.aField = "a value";
console.log([c.message, c.aField, c.count(), c.viewMessage("world")]);

// ["Hello", "a value", 1, "# Hello WORLD"]

Notes

  • Static methods cannot be called via instances count(), if count() was static it would be accessed as C1.S1
  • static attributes are allowed S1 but are accessed as className.attributeName
  • Fields can be declared at class level, F1
  • Fields do not have to be declared at class level F2
  • There is no notion of public/private/etc.
  • The semi-colons at the end of method declarations are optional
  • get must have zero parameters
  • set must have exactly one parameter

Jason covers (inheritance](https://hacks.mozilla.org/2015/08/es6-in-depth-subclassing/) in depth too which adds more complexity/power if you want it. For now, Know that

  • new.target refers to the constructor called with new (possibly useful form a base class)
  • extends xxx does not have to be a class
  • It is possible to extend built in classes, e.g. Array
  • super() in a constructor must be called before referencing this in the constructor (the base does the this allocation)

var, let and const

TD;DR; for new code use let instead of var except where you want a constant, then use const.

var belongs_in_global_namespace = "global";
let global_too = "but cannot be re-declared"

function a_function() {
    const can_only_be_assigned = "at declaration time!";
    let func_scoped = 1;
    var vars_are_func_scoped = "declare_var";

    if (func_scoped) {
        // new scope block for let assignments
        let func_scoped = 3;
        // not for vars_are_func_scoped
        var vars_are_func_scoped = "overwrite declare_var";
    }
    console.log(`func_scoped still ${func_scoped}`);
    console.log(`vars_are_func_scoped changed ${vars_are_func_scoped}`);
}

a_function();
// func_scoped still 1
// vars_are_func_scoped changed overwrite declare_var

Notes

  • Once declared a const value cannot be re-declared or re-assigned
  • A let variable cannot be re-declared but can be re-assigned

Modules, import/export

Module support is static, i.e. no conditional import or export. Use an existing module system for that (for now).

Exporting

Exports are explicit and can appear anywhere outside of a function/class in a file. Elements can be exported individually or as a group. Elements can be aliased during export.

// a-module.js
function fn1() {console.log("a-module::fn1");}
function fn2() {console.log("a-module::fn2");}
class c1{constructor() {console.log("a-module::c1 constructor");}}
export function fn3() {console.log("a-module::fn3");}
export {fn2, c1 as Class1};
export const happy = true;
export default {
    happy: happy,
    fn2: fn2,
    other: () => "hello"
};

fn1 is not exported, everything else is explicitly exported in various ways. Class c1 is exported with an alternate name Class1.

The export default {} block allows for the ‘default’ import (see next section).

The syntax export XXX {} and export {} as XXX are equivalent.

Importing

Import explicit elements or everything. Aliasing is allowed.

// main.js
import {fn2 as someFn, happy, Class1} from "./a-module.js";
import mod from "./a-module.js"

function ready(){
    someFn();
    var x = new Class1();
    console.log(`happy = ${happy}`);
    document.getElementById("id").innerText = mod.other();
}

// for demo in browser
if (document.readyState != 'loading') {ready();}
else if (document.addEventListener) {
    document.addEventListener('DOMContentLoaded', ready);
}

with a little index.html;

<head>
    <script type="module" src="main.js"></script>
    <script type="module" src="a-module.js"></script>
</head>
<body>  
    <div id="id">waiting...</div>
</body>

Combining index.html, main.js and a-module.js we can demonstrate import/export in a browser.

Note the type="module" in the script tags are needed to make it work otherwise you will see

Uncaught SyntaxError: Unexpected token {

import/export in nodejs (I'm only interested for unit testing) appears to only be available behind an experimental switch. I'll update if I work out how to make use of it.

There is still loads more but I'm caught up enough for now.

  • (Iterators and the for of loop)[https://hacks.mozilla.org/2015/04/es6-in-depth-iterators-and-the-for-of-loop/)
  • (Generators)[https://hacks.mozilla.org/2015/05/es6-in-depth-generators/)
  • (Template strings)[https://hacks.mozilla.org/2015/05/es6-in-depth-template-strings-2/)
  • (Rest parameters and defaults)[https://hacks.mozilla.org/2015/05/es6-in-depth-rest-parameters-and-defaults/)
  • (Destructuring)[https://hacks.mozilla.org/2015/05/es6-in-depth-destructuring/)
  • (Arrow functions)[https://hacks.mozilla.org/2015/06/es6-in-depth-arrow-functions/)
  • (Symbols)[https://hacks.mozilla.org/2015/06/es6-in-depth-symbols/)
  • (Collections)[https://hacks.mozilla.org/2015/06/es6-in-depth-collections/)
  • (Proxies and reflect)[https://hacks.mozilla.org/2015/07/es6-in-depth-proxies-and-reflect/)
  • (Classes)[https://hacks.mozilla.org/2015/07/es6-in-depth-classes/)
  • (Let and const)[https://hacks.mozilla.org/2015/07/es6-in-depth-let-and-const/)
  • (Modules)[https://hacks.mozilla.org/2015/08/es6-in-depth-modules/)