ES6 Handbook

vedanthb

vedanth bora

Posted on June 10, 2022

ES6 Handbook

From var to const/let

In ES5, you declare variables via var. Such variables are function-scoped, their scopes are the innermost enclosing functions. The behavior of varis occasionally confusing. This is an example:

var x = 3;
function func(randomize) {
    if (randomize) {
        var x = Math.random(); // (A) scope: whole function
        return x;
    }
    return x; // accesses the x from line A
}
func(false); // undefined
Enter fullscreen mode Exit fullscreen mode

That func()returns undefined may be surprising. You can see why if you rewrite the code so that it more closely reflects what is actually going on:

var x = 3;
function func(randomize) {
    var x;
    if (randomize) {
        x = Math.random();
        return x;
    }
    return x;
}
func(false); // undefined
Enter fullscreen mode Exit fullscreen mode

In ES6, you can additionally declare variables via let and const. Such variables are block-scoped, their scopes are the innermost enclosing blocks. let is roughly a block-scoped version of var. const works like let, but creates variables whose values can’t be changed.

letand constbehave more strictly and throw more exceptions (e.g. when you access their variables inside their scope before they are declared). Block-scoping helps with keeping the effects of code fragments more local . And it’s more mainstream than function-scoping, which
eases moving between JavaScript and other programming languages.

If you replace var with let in the initial version, you get different behavior:

let x = 3;
function func(randomize) {
    if (randomize) {
        let x = Math.random();
        return x;
    }
    return x;
}
func(false); // 3
Enter fullscreen mode Exit fullscreen mode

That means that you can’t blindly replace var with let or const in existing code; you have to be careful during refactoring.

My advice is:

  • Prefer const. You can use it for all variables whose values never change.
  • Otherwise, use let – for variables whose values do change.
  • Avoid var.

From IIFEs to blocks

In ES5, you had to use a pattern called IIFE (Immediately-Invoked Function Expression) if you wanted to restrict the scope of a variable tmp to a block:

(function () {  // open IIFE
    var tmp = ···;
    ···
}());  // close IIFE

console.log(tmp); // ReferenceError
Enter fullscreen mode Exit fullscreen mode

In ECMAScript 6, you can simply use a block and a letdeclaration (or a const declaration):

{  // open block
    let tmp = ···;
    ···
}  // close block

console.log(tmp); // ReferenceError
Enter fullscreen mode Exit fullscreen mode

From concatenating strings to template literals

With ES6, JavaScript finally gets literals for string interpolation and multi-line strings.

String interpolation

In ES5, you put values into strings by concatenating those values and string fragments:

function printCoord(x, y) {
    console.log('('+x+', '+y+')');
}
Enter fullscreen mode Exit fullscreen mode

In ES6 you can use string interpolation via template literals:

function printCoord(x, y) {
    console.log(`(${x}, ${y})`);
}
Enter fullscreen mode Exit fullscreen mode

Multi-line strings

Template literals also help with representing multi-line strings.

For example, this is what you have to do to represent one in ES5:

var HTML5_SKELETON =
    '<!doctype html>\n' +
    '<html>\n' +
    '<head>\n' +
    '    <meta charset="UTF-8">\n' +
    '    <title></title>\n' +
    '</head>\n' +
    '<body>\n' +
    '</body>\n' +
    '</html>\n';
Enter fullscreen mode Exit fullscreen mode

If you escape the newlines via backslashes, things look a bit nicer (but you still have to explicitly add newlines):

var HTML5_SKELETON = '\
    <!doctype html>\n\
    <html>\n\
    <head>\n\
        <meta charset="UTF-8">\n\
        <title></title>\n\
    </head>\n\
    <body>\n\
    </body>\n\
    </html>';
Enter fullscreen mode Exit fullscreen mode

ES6 template literals can span multiple lines:

const HTML5_SKELETON = `
    <!doctype html>
    <html>
    <head>
        <meta charset="UTF-8">
        <title></title>
    </head>
    <body>
    </body>
    </html>`;
Enter fullscreen mode Exit fullscreen mode

From function expressions to arrow functions

In current ES5 code, you have to be careful with this whenever you are using function expressions. In the following example, I create the helper variable _this (line A) so that the this of UiComponent can be accessed in line B.

function UiComponent() {
    var _this = this; // (A)
    var button = document.getElementById('myButton');
    button.addEventListener('click', function () {
        console.log('CLICK');
        _this.handleClick(); // (B)
    });
}
UiComponent.prototype.handleClick = function () {
    ···
};
Enter fullscreen mode Exit fullscreen mode

In ES6, you can use arrow functions, which don’t shadow this (line A):

function UiComponent() {
    var button = document.getElementById('myButton');
    button.addEventListener('click', () => {
        console.log('CLICK');
        this.handleClick(); // (A)
    });
}
Enter fullscreen mode Exit fullscreen mode

(In ES6, you also have the option of using a class instead of a constructor function. That is explored later.)

Arrow functions are especially handy for short callbacks that only return results of expressions.

In ES5, such callbacks are relatively verbose:

var arr = [1, 2, 3];
var squares = arr.map(function (x) { return x * x });
Enter fullscreen mode Exit fullscreen mode

In ES6, arrow functions are much more concise:

const arr = [1, 2, 3];
const squares = arr.map(x => x * x);
Enter fullscreen mode Exit fullscreen mode

When defining parameters, you can even omit parentheses if the parameters are just a single identifier. Thus: (x) => x * x and x => x * x are both allowed.


Handling multiple return values

Some functions or methods return multiple values via arrays or objects. In ES5, you always need to create intermediate variables if you want to access those values. In ES6, you can avoid intermediate variables via destructuring.****

Multiple return values via arrays

exec() returns captured groups via an Array-like object. In ES5, you need an intermediate variable (matchObjin the example below), even if you are only interested in the groups:

var matchObj =
    /^(\d\d\d\d)-(\d\d)-(\d\d)$/
    .exec('2999-12-31');
var year = matchObj[1];
var month = matchObj[2];
var day = matchObj[3];
Enter fullscreen mode Exit fullscreen mode

In ES6, destructuring makes this code simpler:

const [, year, month, day] = /^(\d\d\d\d)-(\d\d)-(\d\d)$/.exec('2999-12-31');
Enter fullscreen mode Exit fullscreen mode

The empty slot at the beginning of the Array pattern skips the Array element at index zero.

Multiple return values via objects

The method Object.getOwnPropertyDescriptor() returns a property descriptor, an object that holds multiple values in its properties.

In ES5, even if you are only interested in the properties of an object, you still need an intermediate variable (propDesc in the example below):

var obj = { foo: 123 };

var propDesc = Object.getOwnPropertyDescriptor(obj, 'foo');
var writable = propDesc.writable;
var configurable = propDesc.configurable;

console.log(writable, configurable); // true true
Enter fullscreen mode Exit fullscreen mode

In ES6, you can use destructuring:

const obj = { foo: 123 };

const {writable, configurable} = Object.getOwnPropertyDescriptor(obj, 'foo');

console.log(writable, configurable); // true true
Enter fullscreen mode Exit fullscreen mode

From for to forEach() to for-of

Prior to ES5, you iterated over Arrays as follows:

var arr = ['a', 'b', 'c'];
for (var i=0; i<arr.length; i++) {
    var elem = arr[i];
    console.log(elem);
}
Enter fullscreen mode Exit fullscreen mode

In ES5, you have the option of using the Array method forEach():

arr.forEach(function (elem) {
    console.log(elem);
});
Enter fullscreen mode Exit fullscreen mode

A for loop has the advantage that you can break from it, forEach() has the advantage of conciseness.

In ES6, the for-of loop combines both advantages:

const arr = ['a', 'b', 'c'];
for (const elem of arr) {
    console.log(elem);
}
Enter fullscreen mode Exit fullscreen mode

If you want both index and value of each array element, for-of has got you covered, too, via the new Array method entries() and destructuring:

for (const [index, elem] of arr.entries()) {
    console.log(index+'. '+elem);
}
Enter fullscreen mode Exit fullscreen mode

Handling parameter default values

In ES5, you specify default values for parameters like this:

function foo(x, y) {
    x = x || 0;
    y = y || 0;
    ···
}
Enter fullscreen mode Exit fullscreen mode

ES6 has nicer syntax:

function foo(x=0, y=0) {
    ···
}
Enter fullscreen mode Exit fullscreen mode

An added benefit is that in ES6, a parameter default value is only triggered by undefined, while it is triggered by any falsy value in the previous ES5 code.


From arguments to rest parameters

In ES5, if you want a function (or method) to accept an arbitrary number of arguments, you must use the special variable arguments:

function logAllArguments() {
    for (var i=0; i < arguments.length; i++) {
        console.log(arguments[i]);
    }
}
Enter fullscreen mode Exit fullscreen mode

In ES6, you can declare a rest parameter (args in the example below) via the ... operator:

function logAllArguments(...args) {
    for (const arg of args) {
        console.log(arg);
    }
}
Enter fullscreen mode Exit fullscreen mode

Rest parameters are even nicer if you are only interested in trailing parameters:

function format(pattern, ...args) {
    ···
}
Enter fullscreen mode Exit fullscreen mode

Handling this case in ES5 is clumsy:

function format(pattern) {
    var args = [].slice.call(arguments, 1);
    ···
}
Enter fullscreen mode Exit fullscreen mode

Rest parameters make code easier to read: You can tell that a function has a variable number of parameters just by looking at its parameter definitions.


From function expressions in object literals to method definitions

In JavaScript, methods are properties whose values are functions.

In ES5 object literals, methods are created like other properties. The property values are provided via function expressions.

var obj = {
    foo: function () {
        ···
    },
    bar: function () {
        this.foo();
    }, // trailing comma is legal in ES5
}
Enter fullscreen mode Exit fullscreen mode

ES6 has method definitions, special syntax for creating methods:

const obj = {
    foo() {
        ···
    },
    bar() {
        this.foo();
    },
}
Enter fullscreen mode Exit fullscreen mode

From constructors to classes

ES6 classes are mostly just more convenient syntax for constructor functions.

Base classes

In ES5, you implement constructor functions directly:

function Person(name) {
    this.name = name;
}
Person.prototype.describe = function () {
    return 'Person called '+this.name;
};
Enter fullscreen mode Exit fullscreen mode

In ES6, classes provide slightly more convenient syntax for constructor functions:

class Person {
    constructor(name) {
        this.name = name;
    }
    describe() {
        return 'Person called '+this.name;
    }
}
Enter fullscreen mode Exit fullscreen mode

Note the compact syntax for method definitions – no keyword function needed. Also note that there are no commas between the parts of a class.

Derived classes

Subclassing is complicated in ES5, especially referring to super-constructors and super-properties. This is the canonical way of creating a sub-constructor Employee of Person:

Person.call(this, name); // super(name)
    this.title = title;
}

Employee.prototype = Object.create(Person.prototype);

Employee.prototype.constructor = Employee;

Employee.prototype.describe = function () {
    return Person.prototype.describe.call(this) // super.describe()
           + ' (' + this.title + ')';
};
Enter fullscreen mode Exit fullscreen mode

ES6 has built-in support for subclassing, via the extends clause:

class Employee extends Person {
    constructor(name, title) {
        super(name);
        this.title = title;
    }
    describe() {
        return super.describe() + ' (' + this.title + ')';
    }
}
Enter fullscreen mode Exit fullscreen mode

From custom error constructors to subclasses of Error

In ES5, it is impossible to subclass the built-in constructor for exceptions, Error. The following code shows a work-around that gives the constructor MyError important features such as a stack trace:

function MyError() {
    // Use Error as a function
    var superInstance = Error.apply(null, arguments);
    copyOwnPropertiesFrom(this, superInstance);
}

MyError.prototype = Object.create(Error.prototype);

MyError.prototype.constructor = MyError;

function copyOwnPropertiesFrom(target, source) {

    Object.getOwnPropertyNames(source)
    .forEach(function(propKey) {
        var desc = Object.getOwnPropertyDescriptor(source, propKey);
        Object.defineProperty(target, propKey, desc);
    });

    return target;
};
Enter fullscreen mode Exit fullscreen mode

In ES6, all built-in constructors can be subclassed, which is why the following code achieves what the ES5 code can only simulate:

class MyError extends Error {
.....
}
Enter fullscreen mode Exit fullscreen mode

From objects to Maps

Using the language construct object as a map from strings to arbitrary values (a data structure) has always been a makeshift solution in JavaScript. The safest way to do so is by creating an objectwhose prototype is null.

Then you still have to ensure that no key is ever the string '__proto__', because that property key triggers special functionality in many JavaScript engines.

The following ES5 code contains the function countWords that uses the object dict as a map:

var dict = Object.create(null);

function countWords(word) {
    var escapedWord = escapeKey(word);
    if (escapedWord in dict) {
        dict[escapedWord]++;
    } else {
        dict[escapedWord] = 1;
    }
}

function escapeKey(key) {
    if (key.indexOf('__proto__') === 0) {
        return key+'%';
    } else {
        return key;
    }
}
Enter fullscreen mode Exit fullscreen mode

In ES6, you can use the built-in data structure Map and don’t have to escape keys. As a downside, incrementing values inside Maps is less convenient.

const map = new Map();

function countWords(word) {
    const count = map.get(word) || 0;
    map.set(word, count + 1);
}
Enter fullscreen mode Exit fullscreen mode

Another benefit of Maps is that you can use arbitrary values as keys, not just strings.


New string methods

The ECMAScript 6 standard library provides several new methods for strings.

From indexOf to startsWith:

if (str.indexOf('x') === 0) {} // ES5
if (str.startsWith('x')) {} // ES6
Enter fullscreen mode Exit fullscreen mode

From indexOfto endsWith:

function endsWith(str, suffix) { // ES5
  var index = str.indexOf(suffix);
  return index >= 0
    && index === str.length-suffix.length;
}

str.endsWith(suffix); // ES6
Enter fullscreen mode Exit fullscreen mode

From indexOf to includes:

if (str.indexOf('x') >= 0) {} // ES5
if (str.includes('x')) {} // ES6
Enter fullscreen mode Exit fullscreen mode

From join to repeat(the ES5 way of repeating a string is more of a hack):

new Array(3+1).join('#') // ES5
'#'.repeat(3) // ES6
Enter fullscreen mode Exit fullscreen mode

New Array methods

There are also several new Array methods in ES6.****

From Array.prototype.indexOf to Array.prototype.findIndex

The latter can be used to find NaN, which the former can’t detect:

const arr = ['a', NaN];

arr.indexOf(NaN); // -1
arr.findIndex(x => Number.isNaN(x)); // 1
Enter fullscreen mode Exit fullscreen mode

As an aside, the new Number.isNaN() provides a safe way to detect NaN (because it doesn’t coerce non-numbers to numbers):

> isNaN('abc')
true
> Number.isNaN('abc')
false
Enter fullscreen mode Exit fullscreen mode

From Array.prototype.slice() to Array.from() or the spread operator

In ES5, Array.prototype.slice() was used to convert Array-like objects to Arrays. In ES6, you have Array.from():

var arr1 = Array.prototype.slice.call(arguments); // ES5

const arr2 = Array.from(arguments); // ES6
Enter fullscreen mode Exit fullscreen mode

If a value is iterable (as all Array-like DOM data structure are by now), you can also use the spread operator (...) to convert it to an Array:

const arr1 = [...'abc'];
    // ['a', 'b', 'c']
const arr2 = [...new Set().add('a').add('b')];
    // ['a', 'b']
Enter fullscreen mode Exit fullscreen mode

From apply() to Array.prototype.fill()

In ES5, you can use apply(), as a hack, to create in Array of arbitrary length that is filled with undefined:

// Same as Array(undefined, undefined)
var arr1 = Array.apply(null, new Array(2));
    // [undefined, undefined]
Enter fullscreen mode Exit fullscreen mode

In ES6, fill() is a simpler alternative:

const arr2 = new Array(2).fill(undefined);
    // [undefined, undefined]
Enter fullscreen mode Exit fullscreen mode

fill()is even more convenient if you want to create an Array that is filled with an arbitrary value:

// ES5
var arr3 = Array.apply(null, new Array(2))
    .map(function (x) { return 'x' });
    // ['x', 'x']

// ES6
const arr4 = new Array(2).fill('x');
    // ['x', 'x']
Enter fullscreen mode Exit fullscreen mode

fill() replaces all Array elements with the given value. Holes are treated as if they were elements.


From CommonJS modules to ES6 modules

Even in ES5, module systems based on either AMD syntax or CommonJS syntax have mostly replaced hand-written solutions such as the revealing module pattern.

ES6 has built-in support for modules. Alas, no JavaScript engine supports them natively, yet. But tools such as browserify, webpack or jspm let you use ES6 syntax to create modules, making the code you write future-proof.

Multiple exports

In CommonJS, you export multiple entities as follows:

//------ lib.js ------
var sqrt = Math.sqrt;
function square(x) {
    return x * x;
}

function diag(x, y) {
    return sqrt(square(x) + square(y));
}

module.exports = {
    sqrt: sqrt,
    square: square,
    diag: diag,
};

//------ main1.js ------
var square = require('lib').square;
var diag = require('lib').diag;

console.log(square(11)); // 121
console.log(diag(4, 3)); // 5
Enter fullscreen mode Exit fullscreen mode

Alternatively, you can import the whole module as an object and access square and diag via it:

//------ main2.js ------
var lib = require('lib');
console.log(lib.square(11)); // 121
console.log(lib.diag(4, 3)); // 5
Enter fullscreen mode Exit fullscreen mode

Multiple exports in ES6

In ES6, multiple exports are called named exports and handled like this:

//------ 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));
}

//------ main1.js ------
import { square, diag } from 'lib';
console.log(square(11)); // 121
console.log(diag(4, 3)); // 5
Enter fullscreen mode Exit fullscreen mode

The syntax for importing modules as objects looks as follows (line A):

//------ main2.js ------
import * as lib from 'lib'; // (A)
console.log(lib.square(11)); // 121
console.log(lib.diag(4, 3)); // 5
Enter fullscreen mode Exit fullscreen mode

Single exports

Node.js extends CommonJS and lets you export single values from modules, via module.exports:

//------ myFunc.js ------
module.exports = function () { ··· };

//------ main1.js ------
var myFunc = require('myFunc');
myFunc();
Enter fullscreen mode Exit fullscreen mode

In ES6, the same thing is done via a so-called default export(declared via export default):

//------ myFunc.js ------
export default function () { ··· } // no semicolon!

//------ main1.js ------
import myFunc from 'myFunc';
myFunc();
Enter fullscreen mode Exit fullscreen mode

I hope this blog helped you learn something new.

💖 💪 🙅 🚩
vedanthb
vedanth bora

Posted on June 10, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

Javascript Scopes
javascript Javascript Scopes

June 26, 2022

ES6 Handbook
javascript ES6 Handbook

June 10, 2022

Var vs let and const; easy as pie
javascript Var vs let and const; easy as pie

January 27, 2021