Frontend most useful features of ES6/ES2015

3 months 2 weeks ago

JavaScript is the programming language of HTML and the Web. It was invented by Brendan Eich during a ten days in order to accommodate the Navigator 2.0 Beta release schedule in 1995, and became an ECMA standard in 1997. ECMA-262 is the official name of the standard. ECMAScript is the official name of the language.

ES6 brings a lot of new features for JavaScript developers. Here I share with you some of its core features.

Classes

JavaScript’s class is (mostly) just syntactical sugar for prototypes, which are very different from traditional classes and it does not have classes, the way that Java and other languages have classes.

In traditional class-oriented languages, you create classes, which are templates for objects. When you want a new object, you instantiate the class, which tells the language engine to copy the methods and properties of the class into a new entity, called an instance. The instance is your object, and, after instantiation, has absolutely no active relation with the parent class.

JavaScript does not have such copy mechanics. “Instantiating” a class in JavaScript does create a new object, but not one that is independent of its parent class.

Rather, it creates an object that is linked to a prototype. Changes to that prototype propagate to the new object, even after instantiation.

1

2

3

4

5

6

7

8

9

10

11

12

class Auto {

    constructor(data) {

        this.make = data.make;

        this.model = data.model;

        this.year = data.year;

        this.price = data.price;

    }

 

    getDetails() {

        return `${this.year} ${this.make} ${this.model}`;

    }

}

Classes created with extends are called subclasses, or derived classes. Using them is straightforward. Building on our Auto example:

1

2

3

4

5

6

7

8

9

10

class Car extends Auto {

    constructor(data) {

        super(data);

        this.isElectric = data.isElectric;

        this.isHatchback = data.isHatchback;

    }

    getDetails() {

        return `${super.getDetails()} Electric: ${this.isElectric} Hatchback: ${this.isHatchback}`;

    }

}

 

Default Params

Default function parameters allow formal parameters to be initialized with default values if no value or undefined is passed. In JavaScript, parameters of functions default to undefined. However, in some situations it might be useful to set a different default value. This is where default parameters can help.

default parameter value is specified for a parameter via an equals sign (=). If a caller doesn’t provide a value for the parameter, the default value is used.

Old ES5 way to set default params

 

1

2

3

4

5

6

7

// So there is a better way to do this, it checks param is actually undefined or not:

function link(height, color, callbackFn) {

  var height = typeof height !== 'undefined' ?  height : 50;

  var color = typeof color !== 'undefined' ?  color : 'red';

  var callbackFn = typeof callbackFn !== 'undefined' ?  callbackFn : function() {};

  // function content...

}

 

ES6 way to write default params

 

1

2

3

4

// or using ES6 const and let

const noop = () => {}

const link = (height = 50, color = 'red', callbackFn = noop) => {

  // function content...

}

 

Rest parameters

If you prefix a parameter name with the rest operator (…), that parameter receives all remaining parameters via an Array:

1

2

3

4

5

6

7

function format(pattern, ...params) {

    return {pattern, params};

}

format(1, 2, 3);  

// { pattern: 1, params: [ 2, 3 ] }

format();

// { pattern: undefined, params: [] }

 

Spread operator (…)

In function and constructor calls, the spread operator turns iterable values into arguments:

1

2

3

4

Math.max(-1, 5, 11, 3)  // 11

Math.max(...[-1, 5, 11, 3])  // 11

Math.max(-1, ...[-1, 5, 11], 3)  // 11

[1, ...[2,3], 4]  // [1, 2, 3, 4]

 

Parameter handling as destructuring

The ES6 way of handling parameters is equivalent to destructuring the actual parameters via the formal parameters. That is, the following function call:

1

2

3

4

5

6

function logSum(x=0, y=0) { console.log(x + y); }

logSum(7, 8);

becomes:

{ let [x=0, y=0] = [7, 8];

{ console.log(x + y); }

}

 

Parameter default values

ECMAScript 6 lets you specify default values for parameters:

1

function f(x, y=0) { return [x, y]; }

Omitting the second parameter triggers the default value:

1

2

3

4

f(1) //result [1, 0]

f()  //result [undefined, 0]

Watch out – undefined triggers the default value, too:

f(undefined, undefined)  //return [undefined, 0]

 

Rest parameters

Putting the rest operator (…) in front of the last formal parameter means that it will receive all remaining actual parameters in an Array.

1

2

3

4

5

function f(x, ...y) {

    ···

}

 

f('a', 'b', 'c'); // x = 'a'; y = ['b', 'c']

If there are no remaining parameters, the rest parameter will be set to the empty Array:

1

2

f();

// x = undefined; y = []

 

Simulating named parameters

When calling a function (or method) in a programming language, you must map the actual parameters (specified by the caller) to the formal parameters (of a function definition). There are two common ways to do so:

  • Positional parameters are mapped by position. The first actual parameter is mapped to the first formal parameter, the second actual to the second formal, and so on:

 

1

selectEntries(3, 20, 2)

 

  • Named parameters use names (labels) to perform the mapping. Formal parameters have labels. In a function call, these labels determine which value belongs to which formal parameter. It does not matter in which order named actual parameters appear, as long as they are labeled correctly. Simulating named parameters in JavaScript looks as follows.

 

1

selectEntries({ start: 3, end: 20, step: 2 })

Named parameters have two main benefits: they provide descriptions for arguments in function calls and they work well for optional parameters.

The spread operator (…)

The spread operator (…) looks exactly like the rest operator, but is its opposite:

  • Rest operator: collects the remaining items of an iterable into an Array and is used for rest parameters and destructuring.
  • Spread operator: turns the items of an iterable into arguments of a function call or into elements of an Array.

 

1

2

3

4

const arr1 = ['a', 'b'];

const arr2 = ['c', 'd'];

arr1.push(...arr2);  

// arr1 is now ['a', 'b', 'c', 'd']

The spread operator can also be used inside Array literals:

1

2

[1, ...[2,3], 4]  

// [1, 2, 3, 4]

That gives you a convenient way to concatenate Arrays:

1

2

3

4

5

const x = ['a', 'b'];

const y = ['c'];

const z = ['d', 'e'];

const arr = [...x, ...y, ...z];

// ['a', 'b', 'c', 'd', 'e']

 

Template Literals

In traditional JavaScript, text that is enclosed within matching ” marks, or ‘ marks is considered a string. Text within double or single quotes can only be on one line. There was also no way to insert data into these strings. This resulted in a lot of ugly concatenation code that looked like:

Old ES5 way

1

2

3

4

var name = 'Sam';

var age = 42;

console.log('hello my name is ' + name + ' I am ' + age + ' years old');

//  'hello my name is Sam I am 42 years old'

ES6 way  

ES6 introduces a new type of string literal that is marked with back ticks (`). These string literals can include newlines, and there is a new mechanism for inserting variables into strings:

1

2

3

4

var name = 'Sam';

var age = 42;

console.log(`hello my name is ${name}, and I am ${age} years old`);

//  'hello my name is Sam, and I am 42 years old'

 

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:

1

2

3

4

5

6

7

8

9

10

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';

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

1

2

3

4

5

6

7

8

9

10

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>';

ES6 template literals can span multiple lines:

1

2

3

4

5

6

7

8

9

10

const HTML5_SKELETON = `

    <!doctype html>

    <html>

    <head>

        <meta charset="UTF-8">

        <title></title>

    </head>

    <body>

    </body>

    </html>`;

 

Destructuring

Destructuring is a way to quickly extract data out of an Object or Array without having to write much code.

ES5 way

1

2

3

4

5

6

7

8

let foo = ['one', 'two', 'three'];

let one   = foo[0];

let two   = foo[1];

let three = foo[2];

ES6 way Destructuring    

let foo = ['one', 'two', 'three'];

let [one, two, three] = foo;

console.log(one); //  'one'

Destructuring can also be used for passing objects into a function, allowing you to pull specific properties out of an object in a concise manner. It is also possible to assign default values to destructured arguments, which can be a useful pattern if passing in a configuration object.

1

2

3

4

5

6

7

let jane = { firstName: 'Jane', lastName: 'Doe'};

let john = { firstName: 'John', lastName: 'Doe', middleName: 'Smith' }

function sayName({firstName, lastName, middleName = 'N/A'}) {

  console.log(`Hello ${firstName} ${middleName} ${lastName}`)  

}

sayName(jane) //  Hello Jane N/A Doe

sayName(john) //  Hello John Smith Doe

The spread operator allows an expression to be expanded in places where multiple arguments (for function calls) or multiple elements (for array literals) or multiple variables (for destructuring assignment) are expected.

For Arrays:

1

2

3

4

5

6

7

8

9

const fruits = ['apple', 'banana'];

const veggies = ['cucumber', 'potato'];

const food = ['grapes', ...fruits, ...veggies];

//  ["grapes", "apple", "banana", "cucumber", "potato"]

const [fav, ...others] = food;

console.log(fav);

//  "grapes"

console.log(others);

//  ["apple", "banana", "cucumber", "potato"]

For Objects:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

const member = {

  name: 'Ben',

  title: 'software developer',

  skills: ['javascrip:t', 'react', 'redux'],

};

const memberWithMetadata = {

  ...member,

  previousProjects: ['ProjectWhite', 'ProjectRed'];

};

// behind the scenes:

const memberWithMetadata = Object.assign(member, {previousProjects: ['ProjectWhite', 'ProjectRed']});

console.log(memberWithMetadata.name);

// -> "Ben"

console.log(Object.keys(memberWithMetadata));

// -> ["apple", "banana", "cucumber", "potato"]

For function calls:

1

2

3

4

const food = ['grapes', 'apple', 'banana', 'cucumber', 'potato'];

function eat() { console.log(...arguments); }

eat(...food)  

//  grapes apple banana cucumber potato

 

Arrow Functions

Arrow functions – also called “fat arrow” functions. An arrow function expression has a shorter syntax than a function expression.

They utilize a new token, =>, that looks like a fat arrow and does not bind its own this, arguments, super, or new.target. this is no longer referring to the scope inside of the function. It’s referring to the scope that’s outside of the function.

Arrow functions are always anonymous. They cannot be used as constructors.

The arrow function example above allows a developer to accomplish the same result with fewer lines of code and approximately half of the typing.

// ES5

1

2

3

items.forEach(function(x) {

    incrementedItems.push(x+1)

})

// ES6

1

2

3

items.forEach((x) => {

    incrementedItems.push(x+1)

})

Functions that calculate a single expression and return its values can be defined even simpler:

// ES6

1

var multiply = (x, y) => { return x * y };

// ES5

1

2

3

var multiply = function(x, y) {

    return x * y;

};

There is one important difference, however: arrow functions do not set a local copy of this, arguments, super, or new.target. When this is used inside an arrow function JavaScript uses the this from the outer scope. Consider the following example:

1

2

3

4

5

6

7

8

9

10

11

12

class Flowers {

  constructor(flowers) {

    this.flowers = Array.isArray(flowers) ? flowers : [];

  }

  outputList() {

    this.flowers.forEach(function(flower, i) {

      console.log(flower, i + '/' + this.flowers.length);  // no this

    })

  }

}

var ctrl = new Flowers(['rose', 'sunflower']);

ctrl.outputList();

Let’s try this code on ES6 Fiddle (http://www.es6fiddle.net/). As we see, this gives us an error, since this is undefined inside the anonymous function.

+

Now, let’s change the method to use the arrow function:

1

2

3

4

5

6

7

8

9

10

11

12

13

class Flowers {

  constructor(flowers) {

    this.flowers = Array.isArray(flowers) ? flowers : [];

  }

  outputList() {

    this.flowers

      .forEach((flower, i) => console

        .log(flower, i + '/' + this.flowers.length);  // `this` works!

    )

  }

}

 

var ctrl = new Toppings(['rose', 'sunflower']);

Here this inside the arrow function refers to the instance variable.

Promises and Callbacks

Code that makes use of asynchronous callbacks or promises often contains a great deal of function and return keywords. When using promises, these function expressions will be used for chaining. Here’s a simple example of chaining promises from the MSDN docs:

// ES5

1

2

3

4

5

6

7

aAsync().then(function() {

    returnbAsync();

}).then(function() {

    returncAsync();

}).done(function() {

    finish();

});

This code is simplified, and arguably easier to read using arrow functions:

// ES6

1

aAsync().then(() => bAsync()).then(() => cAsync()).done(() => finish);

Promises

Promises are built-in ES6.

 

1

2

3

4

5

6

7

const wait (ms) => {

  return new Promise((resolve, reject) => {

    setTimeout(resolve, ms);

  });

}

 

wait(3000).then(() => console.log('tick'));

 

Promises vs Callbacks

For HTTP Requests, our existing solution is to use callbacks:

1

2

3

4

request(url, (error, response) => {

  // handle success or error.

});

doSomethingElse();

A few problems exist with callbacks. One is known as “Callback Hell”. A larger problem is decomposition.

The callback pattern requires us to specify the task and the callback at the same time. In contrast, promises allow us to specify and dispatch the request in one place:

promise = fetch(url); //fetch is a replacement for XMLHttpRequest

and then to add the callback later, and in a different place:

1

2

3

promise.then(response => {

  // handle the response.

});

This also allows us to attach multiple handlers to the same task:

1

2

3

4

5

6

promise.then(response => {

  // handle the response.

});

promise.then(response => {

  // do something else with the response.

});

 

More on Promises

.then() always returns a promise. Always.

1

2

3

4

p1 = getDataAsync(query);

 

p2 = p1.then(

  results => transformData(results));

p2 is now a promise regardless of what transformData() returned. Even if something fails.

If the callback function returns a value, the promise resolves to that value:

1

p2 = p1.then(results => 1);

p2 will resolve to “1”.

If the callback function returns a promise, the promise resolves to a functionally equivalent promise:

1

2

3

4

p2 = p1.then(results => {

  let newPromise = getSomePromise();

  return newPromise;

});

p2 is now functionally equivalent to newPromise.

1

2

3

4

5

6

7

p2 = p1.then(

  results => throw Error('Oops'));

 

p2.then(results => {

  // You will be wondering why this is never

  // called.

});

p2 is still a promise, but now it will be rejected with the thrown error.

Why won’t the second callback ever be called?

Catching Rejections

The function passed to then takes a second argument, i.e. error, which represents error catching within the promise chain.

1

2

3

4

5

6

7

8

9

10

11

fetch('http://ngcourse.herokuapp.com/api/v1/tasks')

  .then(response => response.data)

  .then(tasks => filterTasksAsynchronously(tasks))

  .then(tasks => {

    $log.info(tasks);

    vm.tasks = tasks;

  })

  .then(

    null,

    error => log.error(error)

  );

Note that one catch at the end is often enough.

let and const

ES6 introduces the concept of block scoping. Block scoping will be familiar to programmers from other languages like C, Java, or even PHP. In ES5 JavaScript, and earlier, vars are scoped to functions, and they can “see” outside their functions to the outer context.

1

2

3

4

5

6

7

8

9

10

11

12

var five = 5;

var threeAlso = three; // error

function scope1() {

  var three = 3;

  var fiveAlso = five; // == 5

  var sevenALso = seven; // error

}

function scopt2() {

  var seven = 7;

  var fiveAlso = five; // == 5

  var threeAlso = three; // error

}

In ES5 functions were essentially containers that could be “seen” out of, but not into.

In ES6 var still works that way, using functions as containers, but there are two new ways to declare variables: const, and let. const, and let use {, and } blocks as containers, hence “block scope”.

Block scoping is most useful during loops. Consider the following:

1

2

3

4

5

6

7

var i;

for (i = 0; i < 10; i += 1) {

  var j = i;

  let k = i;

}

console.log(j); // 9

console.log(k); // undefined

Despite the introduction of block scoping, functions are still the preferred mechanism for dealing with most loops.

let works like var in the sense that its data is read/write. let is also useful when used in a for loop. For example, without let:

1

2

3

for(var x=0;x<5;x++) {

  setTimeout(()=>console.log(x), 0)

}

Would output 5,5,5,5,5. However, when using let instead of var, the value would be scoped in a way that people would expect.

1

2

3

for(let x=0;x<5;x++) {

  setTimeout(()=>console.log(x), 0)

}

Alternatively, const is read only. Once const has been assigned, the identifier can not be re-assigned, the value is not immutable.

For Example:

1

2

3

4

5

const myName = 'pat';

let yourName = 'jo';

 

yourName = 'sam'; // assigns

myName = 'jan';   // error

The read only nature can be demonstrated with any object:

1

2

3

const literal = {};

literal.attribute = 'test'; // fine

literal = []; // error;

 

Modules

ES6 is the first time that JavaScript has built-in modules which works in a similar way to other languages.

ES6 modules are stored in files. There is exactly one module per file and one file per module. You have two ways of exporting things from a module. These two ways can be mixed, but it is usually better to use them separately.

Named exports

 

1

2

3

4

5

6

7

//------ lib.js ------

export const sqrt = Math.sqrt;

export function square(x) { return x * x; }

 

//------ main.js ------

import { square } from 'lib';

console.log(square(11)); // 121

You can also import the complete module:

1

2

3

//------ main.js ------

import * as lib from 'lib';

console.log(lib.square(11)); // 121

 

Single default export

There can be a single default export. For example, a function:

1

2

3

4

5

6

//------ myFunc.js ------

export default function () { ··· } // no semicolon!

 

//------ main1.js ------

import myFunc from 'myFunc';

myFunc();

Note that there is no semicolon at the end if you default-export a function or a class (which are anonymous declarations).

The basics of ES6 modules

There are two kinds of exports: named exports (several per module) and default exports (one per module). It is possible use both at the same time, but usually best to keep them in separate.

ECMAScript 6 provides several styles of exporting:

Re-exporting:

  • Re-export everything (except for the default export):
    • export * from ‘src/other_module’;
  • Re-export via a clause:
    • export { foo as myFoo, bar } from ‘src/other_module’;
    • export { default } from ‘src/other_module’;
    • export { default as foo } from ‘src/other_module’;
    • export { foo as default } from ‘src/other_module’;

Named exporting via a clause:

  • export { MY_CONST as FOO, myFunc };
  • export { foo as default };

Inline named exports:

  • Variable declarations:
    • export var foo;
    • export let foo;
    • export const foo;
  • Function declarations:
    • export function myFunc() {}
    • export function* myGenFunc() {}
  • Class declarations:
    • export class MyClass {}

Default export:

  • Function declarations (can be anonymous here):
    • export default function myFunc() {}
    • export default function () {}
    • export default function* myGenFunc() {}
    • export default function* () {}
  • Class declarations (can be anonymous here):
    • export default class MyClass {}
    • export default class {}
  • Expressions: export values. Note the semicolons at the end.
    • export default foo;
    • export default ‘Hello world!’;
    • export default 3 * 7;
    • export default (function () {});
ECMAScript 6 modules basic goal is to providing following features:
  • Similarity to CommonJS, they have a compact syntax, a preference for single exports and support for cyclic dependencies.
  • Similarity to AMD, they have direct support for asynchronous loading and configurable module loading.
  • Syntax is more compact than CommonJS.
  • Structure can be statically analyzed (for static checking, optimization, etc.).
  • Support for cyclic dependencies is better than CommonJS.
The ES6 module standard has two parts:
  • Declarative (importing and exporting syntax)
  • Programmatic loader API: to configure how modules are loaded and to conditionally load modules
Shahid Hussain

Shahid Hussain is a frontend developer and UX Consultant living and working in Sweden. Shahid is specializes in JavaScript development and developed anything from WordPress websites to complex e-commerce JavaScript applications. Shahid can also sketch, from websites to apps and icons, even print material. He works on content-centric, and mobile products, as well as cross-portal user experiences. Photography, music and travelling can trigger his attention.