ES5 to ES6+ Guide


ECMAScript (ES in ES5 and ES2015 is abbreviation of ECMAScript) is a general purpose scripting language specification standardized set by ECMA-262 of ECMA International. The first build of ECMAScript was distributed in 1997, and it took them 12 years to get to the distribution of widely used ES5 in 2009. Six years later, ECMA International released the sixth build, ECMAScript 2015, not ECMAScript 6 (from here on out ES6) as naturally predicted. Previously it took them longer to publish each build, but in order to make the language more suitable to the fast-changing environment that is programming, it was published with the published year as a part of the name instead of the version number.

ES6 addressed the issues raised in ES5 and in older versions, and added new syntax that allowed for codes to be written in much cleaner form, thereby adding readability and maintainability. Famous online JavaScript libraries (Lodash, React, Vue etc.) also followed suit and adopted the ES6, allowing the new line of JavaScript libraries to be more easily accessible with the ES6 platforms.

As the Evergreen browsers (browsers capable of automatically updating without user intervention) are on the rise globally, ES6 settles its place in the market along with the development of tools that facilitate ES6 to be compatible in ES5 environments (such as older browser environments like IE8.) Developers no longer need to hesitate to adopt ES6 in real world work places. Let’s begin writing trendier codes in ES6!

This guide serves to discuss the benefits of switching over to ES6 and to help the readers better understand the newly published JavaScript libraries. Specifications made after ES2015 are also included, but for the sake of convenience, will collectively be referred to as ES6.

Table of Contents

Cross Browser Programming Using the Transpiler

Most of the following codes will not run on IE. However, it is not to say that ES6 is not compatible with IE. Using a Transpiler (Babel), the user can easily change them into JavaScript codes that are compatible with most of the browsers. Edge, Firefox, Chrome and other browsers support the ES6’s basic class functionality as of 2018 Dec 17.

Babel Transpiler Usage Example

To demonstrate how ES6 codes are morphed into cross browser compatible codes with the help of a transpiler, let's take a look at the following code.

const callName = (person) => {
  console.log(`Hey, ${person.name}`);
};

If the code shown above is ran on IE directly, it will not run and will throw out an error. However, once the code goes through the transpiler, it is transformed into the compatible ES5 code as shown below.

"use strict";

var callName = function callName(person) {
  console.log("Hey, " + person.name);
};

Using Babel – Try It Out, it is possible to run tests and see how the ES6 codes are transformed by the Babel transpiler. Although this guide does not specifically discuss how to apply transpilers onto projects, Dealing with Lower Rank Browsers from [FE Guide] Bundler beautifully explains the step by step process of applying the transpliers.

Transpiler takes care of Cross Browser compatibility. Only thing the developer has to do is to simply code in ES6.

Advantages of let and const

In ES5, keyword var was used to declare a variable. Variables declared by var was always subjected change, so it was implausible to declare constant variables that are immutable. To differentiate constant variables from other variables, developers often resulted in restricting the names of constant variables to only capital letters and underscores. Unlike other programming languages, because variables declared by var is given the scope of a function in JavaScript, it is possible for variables declared by var in if statements and for statements to be accessed from outside of the block. Furthermore, using var allows for hoisting, where a variable can be used before it is declared, to happen. Due to the nature of var keyword, developers face numerous issues programming in JavaScript. As to address the issues mentioned above, let and const keywords were added. Now, let’s consider how these newly added keywords help developers.

Block Scope

Variables declared using let and const take the scope of a block. Contrarily, variables declared by var take the scope of a function, making it prone to unintended changes and therefore, errors. Using let and const to declare variables allows developers to prevent themselves from making such mistakes.

ES5

function sayHello(name) {
  if (!name) {
    var errorMessage = '"name" parameter should be non empty String.';
    
    alert(errorMessage);
  }
  
  console.log('Hello, ' + name + '!');
  console.log(errorMessage); // '"name" parameter should be non empty String.'
}

ES6

function sayHello(name) {
  if (!name) {
    let errorMessage = '"name" parameter should be non empty String.';
    
    alert(errorMessage);
  }
  
  console.log('Hello, ' + name + '!');
  console.log(errorMessage); // ReferenceError
}

Because variables declared using var is added to the scope of currently executing function, the errorMessage variable remains accessible even after the if blcok finishes running. However, variables declared by let and const are added to the block scope, therefore raising a ReferenceError if there is an attempt to access errorMessage from outside of the block.

Addressing the Variable Hoisting in ES5

var keyword leaves room for hoisting, where a variable can be used before it is declared. However, when variables are declared with let and const, it prevents developers from making mistakes by raising an error.

ES5

hello = 'Hello, World!';  // The variable is initialized, but does not cause any errors.

console.log(hello); // 'Hello, World!'

var hello;                // Variable is declared

ES6

hello = 'Hello, World!'; // ReferenceError - variable hello has not been declared

console.log(hello); 

let hello;

In ES5, the variable ‘hello’ does not cause any errors even if the variable is initialized before it is declared. Why is that? JavaScript, before executing any program, first looks for var and function variables and hoists them up to the top of the current scope. Such hoisting causes errors that are hard to detect and consequences that are unintended. On the other hand, when the same code is written using the let keyword of ES6, a ReferenceError is raised because ‘hello’ has not yet been declared.

Using and Characterizing let and const

const is used for values that do not change while the program executes, and let is used for values that do change. Listed below are some examples.

// Value changes
let foo = 'foo';
foo = 'foo2'; // Successful - Value changes are allowed

const bar = 'bar';
bar = 'bar2'; // TypeError - variable bar is a constant; cannot be changed. 

// declaration and initialization
const baz2;  // TypeError - variables declared using const must be initialized

let baz;         // Successful - variables declared using let does not need to be initialized
baz = 'baz';

As examples above demonstrate, variables declared using const cannot change once declared, but variables declared using let can. Therefore, const variable must be initialized when declared, and when an attempt is made to alter the value of a const variable, a TypeError will be raised.

However, just because const was used to declare an object or an array, it does not mean that the properties or elements declared within those objects and arrays cannot be changed.

// Changing the property value of a const variable
const foo2 = {
 bar2: 'bar'
};

foo2.bar2 = 'bar2'; // Successful - properties of foo2 can be changed.

While one could mistakenly believe that properties or elements of objects and arrays declared using const are also fixed, properties and elements can be changed. Developers must be cautious not to overgeneralize the concept of const when using it to declare reference values.

Arrow Function

The newly introduced arrow function provides transparency in the way this binds in functions and simplifies the tedious syntax of function expressions. Arrow function gets its name from the fact that => token used in the function simply looks like an arrow.

Syntax

ES5

var sum = function(a, b) {
  return a + b;
} 

ES6

const sum = (a, b) => {
  return a + b;
};

this Bind

this Bind is a problem that is known to occur in ES5 in which a handler does not behave as intended when a DOM event handler function is executed.

ES5

var buzzer = {
  name: 'Buzzer1',
  addClickEvent: function() {
    this.element = document.getElementById(this.name);

    var logNameAndRemoveButton = function() {
      console.log(this.name + ' buzzing!');
    }

    this.element.addEventListener('click', logNameAndRemoveButton.bind(this)); // When logNameAndRemoveButton is executed, this object uses "bind(this)" to allocate a this object because "this" object is an element object
  }
};

buzzer.addClickEvent();

ES6

const buzzer = {
  name: 'Buzzer1',
  addClickEvent() {
    this.element = document.getElementById(this.name);

    this.element.addEventListener('click', () => {  // The program runs as intended even without having to this binding the buzzerElement
      console.log(this.name + ' buzzing!');
      document.body.removeChild(this.element);
    });
  }
};

buzzer.addClickEvent();

When an event handler function registered to an element is executed, it runs as a non-strict, so when a handler attempts to access this object, the program automatically searches for the event handling element. Therefore when using methods as event handlers, developers had to manually check and bind this objects to the necessary context using something like handler.bind(this). However, using the arrow function, the program executes as developers intended it to run without much trouble. The arrow function lexically binds this to different values based on the context. In other words, this is bound lexically to the code that contains the arrow function.

Shorter is Trendier

Arrow function allows developers to write simple functions in one line. Previously, each returned value had to be prefaced with the return keyword. However, with the rise of arrow functions, return keyword could be omitted if the value the program is trying to return is an expression. To take advantage of the function, get rid of the curly brackets {...} and the return statement and place the expression to be returned after => token. Then, the expression after => will be implicitly returned and executed.

// Using shorter arrow function expressions

const sum = (a, b) => a + b;

console.log(sum(10, 100)); // 110

Such arrow functions can be especially effective when used as callback functions in Array.prototype.map() or Array.prototype.filter() operations.

const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

// Function expression
const numbersOverFive = numers.filter(function(numer) {
  return number > 5;
});

console.log(numbersOverFive); // [6, 7, 8, 9, 10];

//Using arrow function to callback in one line
const numbersOverFive = numbers.fileter(number => number >5)

console.log(numbersOverFive); // [6, 7, 8, 9, 10];

The latter looks much cleaner as it does not pass a function as a parameter.

There are few differences between arrow functions and functions declared using the function keyword. Arrow function is an expression that takes no properties like this, arguments, super, and new.target with respect to its own context. In other words, the arrow function does not allow self-referencing. Instead of having a this in its own context, the arrow function follows the this of the executed context as justified. Therefore, the arrow fucntion should only be used to simplify functions that are not constructors or are parts of a method.

Class

JavaScript is a prototype-based language. Unlike class-based languages, JavaScript relied on using object prototypes to create code reuse and object inheritance to create APIs that are reminiscent of classes. In the ES5 environment, creating classes differed from library to library. However, in ES6, class syntax was added to JavaScript and therefore, united the usage of libraries.

Syntax

class SomeClass {
  static staticClassMethod() {
    // Static Method
  }
  
  constructor() {
  // Constructor function
  }
  
  someMethod() {
    // Class Method
  }
}

const instance = new SomeClass();
instance.someMethod();

someClass.staticClassMethod();

Each class consists of class keyword, class name, constructor() function, methods, extends keyword for inheritance, and the static keyword. When declaring a class, write the class name after the class keyword, and to inherit from members of other classes, include the parent class after the extends keyword. Classes, like functions, can be used both as declarative and as expressive.

Declaring a Class

class SomeClass {
  //class body
}

Prototype Based Classes

Due to the lack of class representation in ES5, developers imitated classes by using constructors to set property values within instances and prototype chains to share methods among all objects. However, such practice was chaotic, because regular functions and constructors were not syntactically different.

ES5

function Person(name) {
  this.name = name;
}

Person.prototype.sayMyName = function() {
  console.log('My name is "' + this.name + '"');
}

var fred = new Person('fred');

Using a class keyword, same could be achieved with much more easily as demonstrated below.

ES6

class Person {
  constructor(name) {
    this.name = name
  }

  sayMyName() {
    console.log(`My name is "${this.name}"`);
  }
}

const fred = new Person('fred');

Program written in ES6 has clear boundaries among class, constructor method, and methods, therefore, significantly improving readability.

Enhanced Object Literal

Shorthand Object Literal

In ES6, if the key text and the value variable are identical, developers only have to type them once.

ES5

var iPhone = '아이폰';
var iPad = '아이패드';
var iMac = '아이맥';

var appleProducts = {
  iPhone: iPhone,
  iPad: iPad,
  iMac: iMac
};
var one = '1';
var two = '2';
var three = '3';

var numbers = {
  one : one,
  two : two,
  three : three
};

ES6

const one = '1';
const two = '2';
const three = '3';

const numbers {one, two, three};

In the program written in ES5, the property name and the value had to be rewritten multiple times separated by colons, but in the program written in ES6, it is clear to see that colons disappeared and the array consists only of predefined variables. In ES6, the newly created object checks to see is the property keys have corresponding variable names and if they do, respective values are assigned to the properties.

Shorthand for Writing Methods

ES6 also allows users to omit the function keyword and the colon : after the method name when defining a method within an object.

ES5

var dog = {
  name: 'Lycos',
  bark: function () {
    console.log('Woof! Woof!')
  }
};

dog.bark(); // 'Woof! Woof!';

ES6

const dog = {
  name: 'Lycos',
  bark() {
    console.log('Woof! Woof!')
  }
};

dog.bark(); // 'Woof! Woof!';

Computed Property Names

In ES5, it was not possible to simultaneously declare and execute. An object had to be initialized before the user manually allocates the initialized value surrounded by the [,] operators. However, in ES6, an executed value itself can now be used as a property key within the object. In order to set an executed value as a property key, simply place the expression surrounded by the [,] operators where the object property key would be.

While in ES5, developers had to first create objects and then access it manually with square brackets [...] to set properties, ES6 now lets developers use computed expression as a property key. In order to do so, just type the expression surrounded by square brackets [...] into where the property key would be.

ES5

var ironMan = 'Iron Man';
var captainAmerica = 'Captain America';

var MarvelHeroes = {};

MarvelHeroes[ironMan] = 'I`m the Iron Man.';
MarvelHeroes['Groot'] = 'I am Groot.';
MarvelHeroes[captainAmerica] = 'My name is Steve Rogers.';
MarvelHeroes['3-D' + 'MAN'] = 'I`m the 3-D Man!';

ES6

const ironMan = 'Iron Man';
const captainAmerica = 'Captain America';

const MarvelHeroes = {
  [ironMan]: 'I`m the Iron Man.',
  ['Groot']: 'I am Groot.',
  [captainAmerica]: 'My name is Steve Rogers.',
  ['3-D' + 'MAN']: 'I`m the 3-D Man!'
}

Template Literal

The syntax for template literal consists of strings surrounded by the backtick (`). Previously, to interpolate variables into a string, developers had to connect each fragments with a plus sign +. However, with the introduction of template literals, the code becomes more concise as string can be interpolated inside itself. To do so, just write ${expression} in the place of the variable.

ES5

var brandName = 'TOAST';
var productName = 'UI';

console.log('Hello ' + brandName + ' ' + productName + '!'); // 'Hello TOAST UI!';

ES6

const brandName = 'TOAST';
const productName = 'UI';

console.log(`Hello ${brandName} ${productName}!`); // 'Hello TOAST UI!';

As the program written in ES6 clearly demonstrates, it is possible to use brandName and productName together as well as use them independently. Also, in ES6, the new line character \n becomes obsolete. ES6 no longer needs a new line character to express a multi-line string.

ES5

var howToDripCoffee = '1. Boil water.\n2. Grind the beans.\n3. Pour the ground beans into the filter, and place the filter on the cup.\n4. Slowly pour boiled water onto the filter.';

ES6

const howToDripCoffee = `1. Boil water.
2. Grind the beans.
3. Pour the ground beans into the filter, and place the filter on the cup.
4. Slowly pour boiled water onto the filter.';

Using Tagged Templates

ES6 now features a newly added tagged templates, which allow easier string manipulation without loading template engine or libraries. To learn more about template literals, Template Literals : Tagged Templates does an excellent job explaining the tagged templates in detail.

While developers can create tagged templaces from scratch, it is worth mentioning that libraries like common-tags have already done the work. The following example written in ES6 may look off because of the lack of indentation, but the stripIndents method from common-tags library "strips Indents" and makes sure that the code runs smoothly. This allows developers to write cleaner codes without having to pay attention to minor details like indents and spacings.

import {stripIndents} from 'common-tags'

stripIndents`
  1. ${'Boil water'}.
  2. Grind the beans.
  3. Pour the ground beans into the filter, and place the filter on the cup.
  4. Slowly pour boiled water onto the filter.
`
// 1. Boil water.
// 2. Grind the beans.
// 3. Pour the ground beans into the filter, and place the filter on the cup.
// 4. Slowly pour boiled water onto the filter.

The program looks much nicer than the example written in ES5!

Destructuring

When programming in JavaScript, it is a common practice to share functions within objects. However, previously, in order to declared the shared property as a variable, each property had to be broken down and declared independently. In ES6, a new feature called destructuring now allows users to declare variables more simply and write more concise codes.

Declaring a Variable

First, this is an example to show how a property of a variable is declared as another independent variable. Object destructuring takes the properties listed in the curly braces {...} and automatically creates new variables with property names and assign them corresponding values. Array destructuring functions similarly. When an element is placed within the square brackets [...], the respective index is assigned to a new variable with the corresponding name.

ES5

function printUserInformation(data) {
  var name = data.name; 
  var age = data.age;
  var hobby = data.hobbies; 
  var favoriteHobby = hobbies[0];  // Had to independently declare each variables.
  
  console.log('Name: ' + name);
  console.log('Age: ' + age);
  console.log('Favorite Hobby :' + favoriteHobby);
  }

ES6

function printUserInformation(data) {
  const {name, age, gender, hobbies} = data; //each variable is destructured automatically
  const[favoriteHobby] = hobbies; 
  
  console.log('Name: ${name}');
  console.log('Age: ${age}');
  console.log('Favorite Hobby: ${favoriteHobby}');
  }

By using the array destructuring, the previous example didn't even had to use square brackets [...] to declare variables. Also, the tedious repetition of var * = data.* is replaced by a concise one line code.

Destructuring Objects as Function Parameters

JavaScript allows developers to take an object as a function parameter and use its properties as independent variables. In this case, the program does not require extra variable declaration, but only asks developers to write the destructuring code in the place of a function parameter declaration. It is also possible for the developer to use a name that is different from the name declared in the respective object.

ES5

function printError(error) {
  var errorCode = error.errorCode;
  var msg = error.errorMessage;
  
  console.log('Error Code: ' + errorCode);
  console.log('Message: ' + msg);
  }

ES6

function printError({
  errorCode, 
  errorMessage: msg
}) {
  console.log('Error code: ${errorCode}');
  console.log('Message: ${msg}');
}

The repetitions in the program written in ES5, like var * = data.*, disappeared, and now contains cleaner code like that of object literal. Also, nothing else was additionally declared by destructuring the parameters of printError() function. Finally, it is also possible to declare the errorMessage property of the parameter using a different name with the help of a colon :.

Default Value Setting for Function Parameters

Default Value Setting

JavaScript now supports default value setting for function parameters. It allows the parameters to assign a predetermined value when one or more parameters were passed as undefined in order to prevent internal errors. In ES5, developers had to manually check with an if statement if any of the parameters were undefined, and provide the program with a suitable substitute. However, ES6 presents a more delicate solution.

ES5

function sayName(name) {
  if (!name) {
    name = 'World';
  }

  console.log('Hello, ' + name + '!');
}

sayName(); // "Hello, World!"

ES6

const sayName = (name = 'World') => {
  console.log(`Hello, ${name}!`);
}

sayName(); // "Hello, World!"

Default Value Setting with Destructuring

Material handled above is a primitive example of setting default values for function parameters. With ES6, it is now possible to set much more intricate default cases. In ES5, in order to handle default parameters, developers had to write optional codes that were longer than the function itself. Each property had to be given an option so that the program does not stop executing when a value is not given. However, with ES6, developers can easily set default values for multiple parameters using destructuring and property shorthand.

ES5

function drawES5Chart(options) {
  options = options || {};
  var size = options.size || 'big';
  var cords = options.cords || {x: 0, y: 0};
  var radius = options.radius || 25;
  console.log(size, cords, radius);
  // now finally do some chart drawing
}

drawES5Chart({
  cords: {x: 18, y: 30},
  radius: 30
});

ES6

function drawES6Chart({size = 'big', cords = {x: 0, y: 0}, radius = 25} = {}) {
  console.log(size, cords, radius);
  // do some chart drawing
}

drawES6Chart({
  cords: {x: 18, y: 30},
  radius: 30
});

Source: MDN Destructuring_assignment

Warning!

There is a catch to setting default values to function parameters. What if the project only calls for the property value of a parameter object to be set as default? How should one go about dealing with nested defaults?

Bad

function drawES6Chart({size = 'big', coords = {src: {x: 0, y: 0}, dest: {x: 0, y: 0}}, radius = 25} = {}) {
  console.log(size, coords.src.x, coords.src.y, coords.dest.x, coords.dest.y, radius);
}


drawES6Chart({
  coords: {src: {x: 18, y: 30}},
  radius: 30
}); // Error: Attempted to reference undefined x and y.

The example looks like it should execute smoothly, but it does not. Why does the program raise an error? Let’s take a deeper look into it. The coords passed through as a parameter already has a {src: {x: 18, y: 30}} as a value. Therefore, it does not go through the default value setter when the function runs. When the program attempts to access the x and y properties of coords.dest, it will run into undefined, and the program will rightfully throw out a TypeError.

As the example demonstrates, the default value setting only supports non-nested elements. In order to execute the program above without running into an error, it is required to specify options for nested parameters as shown in the ES5 example.

Good

function drawES6Chart({size = 'big', coords = {src: {x: 0, y: 0}, dest: {x: 0, y: 0}}, radius = 25} = {}) {
  if (coords.src === undefined) {
    cords.src = {x: 0, y: 0};
  }
  if (coords.dest === undefined) {
    coords.dest = {x: 0, y: 0};
  }

  console.log(size, coords.src.x, coords.src.y, coords.dest.x, coords.dest.y, radius);
}


drawES6Chart({
  coords: {src: {x: 18, y: 30}},
  radius: 30
}); // Successful

If developers only be extra cautious when dealing with default value setting for nested parameters, ES6 provides tools to write cleaner and more readable codes.

Rest Parameter and Spread Operator

ES6 massively upgraded the accessibility of object literals and array literals. Rest parameters and spread operators are such cases.

Spread Operator

Each array element or an object property of ids is copied to respective locations when …ids is written inside an array or an object literal. Spread operator is used to call functions or inside of array-object literals.

Spreading an Array

Using spread operator, an array can easily be expanded into a list of parameters.

ES5

var friends = ['Jack', 'Jill', 'Tom'];

textToFriends(friends[0], friends[1], friends[2]);

ES6

const friends = ['Jack', 'Jill', 'Tom'];

textToFriends(...friends);

Expanding an Array and and Object

Using ES6, it is easier to add elements of an existing array to a newly created array or to add properties of an existing object to a newly created object. Spread operator simply pastes the existing items in an array or an object into a new array or a new object.

ES5

var friends = ['Jack', 'Jill', 'Tom'];
var moreFriends = [friends[0], friends[1], friends[2], 'Kim'];

var drinks = {
  coffee: 'coffee',
  juice: 'orange juice'
};
var newDrinks = {
  coffee: drinks.coffee,
  juice: 'tomato juice',
  water: 'water'
};

ES6

const friends = ['Jack', 'Jill', 'Tom'];
const moreFriends = [...friends, 'Kim'];

const drinks = {
  coffee: 'coffee',
  juice: 'orange juice'
};
const newDrinks = {
  ...drinks,
  juice: 'tomato juice',
  water: 'water'
};

Rest Parameter

Previously when dealing with unknown number of parameters, developers had to use arguments object like an array. Now, with rest parameters, it is much easier to change parameters into arrays.

ES5

function textToFriends() {
  var message = arguments[0];
  var friends = [].slice.call(arguments, 1); // arguments 2번째 요소부터 친구들 배열로 만들기.

  console.log(message, friends);
}
function textToFriends() {
  var message = arguments[0];
  var friends = [].slice.call(arguments, 1); // create an array starting from the second element of arguments 

  console.log(message, friends);
}

ES6

function textToFriends(message, ...friends) {
  console.log(message, friends);
}

Because developers no longer have to use indexes to separate and create arrays from arguments, as they did in ES5, it is much simpler and more precise to separate parameters into a variable and an array.

Generator

In ES6, a generator is declared using Generator constructor or a function* keyword. Generator is a feature that allows users to step outside of the code midway and continue from where it stopped.

Syntax

function* gen() {
  yield 1;
  yield 2;
  yield 3;
  yield 4;
}

const g = gen();

Source: MDN - Generator

When a generator is executed, the program runs until it is faced with a yield statement and the program is put on hold until the control-flow allows the iterator to continue. In order to jump start the generator on hold, the g.next() method of the generator object is used, and when g.next() is called, the program starts again from where it left off until it meets another yield statement. Then the control-flow jumps back out of the generator and continues the line after the g.next() method. The return value of g.next() is an object, and it consists of aforementioned done Boolean and the value property which is the executed value of the yield statement.

function* gen() {
  yield 1;
  yield 2;
  yield 3;
  yield 4;
}

const g = gen();

console.log(g.next().value); // 1
console.log(g.next().value); // 2
console.log(g.next().value); // 3
console.log(g.next().value); // 4
console.log(g.next().value); // undefined

The last g.next().value is undefined because all yield statements and the generator have finished running.

Source: MDN - Generator Source: MDN - function*

Promise

Promise is an abstract objects that represents asynchronous operations. It tells the user whether the asynchronous operation successfully ran or failed, and returns the value from the operation. Using a promise, developers can easily design functions to use for both cases: when the asynchronous operation succeeds or when it fails. Also, promise allows users to simplify the "callback hell," where a nested callback function calls another function that is nested.

Syntax

const p = new Promise((resolve, reject) => {
  // asynchronous operation
});

p.then(onFulfilled, onRejected).catch(errorCallback);

The function passed onto the promise constructor is known as the executor, and it is executed before the promise constructor returns the constructed object. Both resolve and reject are functions that are passed onto the executor as parameters, and they either resolve or reject the promise. Given the two inputs, the executor judges the eventual completion of the internal asynchronous operation, and passes on the value to either resolve function or reject function so that the promise can be fulfilled. If the resolve is executed inside of the executor, the value is passed on to onFulfilled, the first input in then, and if the reject function is executed, the value is passed on to onRejected.

const checkNumIsExceedTen = new Promise((resolve, reject) => {
  setTimeout(() => {
    const num = getRandomNumberFromServer();

    if(Number.isNaN(num)) {
      throw new Error('Value that from server must be a "number" type.');
    }

    if (num > 10) {
      resolve(num);
    } else {
      reject(num);
    }
  });
});

checkNumIsExceedTen
  .then((num) => {
    console.log(`'num' is ${num}. It is exceed 10.`);
  }, (num) => {
    console.log(`'num' is ${num}. It is not exceed 10.`);
  })
  .catch((error) => {
    console.log(error.message);
  });

The example code creates a promise object that checks to see if the random num variable from the server is bigger than 10. The callbacks to be executed after the promise finished running are registered in then, and a callback to handle the possible error is registered in catch.

Solving the Pyramid of Doom

In order to examine the value of asynchronous operation in ES5, developers used a technique frequently referred to as a “callback hell” or a “Pyramid of Doom.” Such technique is executed by designing multiple layers of callback functions to return the value of the asynchronous operation, and taking the value of the inner-most return value of the callback function.

ES5

doSomething(function(result) {
  doSomethingElse(result, function(newResult) {
    doThirdThing(newResult, function(finalResult) {
      console.log('Got the final result: ' + finalResult);
    }, failureCallback);
  }, failureCallback);
}, failureCallback);

ES6

doSomethingPromise
  .then((result) => doSomethingElse(result))
  .then((newResult) => doThirdThing(newResult))
  .then((finalResult) => console.log('Got the final result: ' + finalResult))
  .catch(failureCallback);

The callback codes actually look readable now. Let's compare the two codes. If the Pyramid of Doom transferred callback every turn to deal with errors, the promise does the very same with one single catch. Also, compared to multi-layered callback functions, promise makes the asynchronous operations much more readable and sequential.

Dealing with Multiple Promises

Promise is an object that is designed to deal with a single asynchronous request. In order to deal with multiple asynchronous requests, the number of promises must also increase. To deal with collection of promises, Promise.all and Promise.race can execute the supplied collection according to the state of executed objects. Iterable objects like arrays are given to the function as parameters. Promise.all waits for every single promise to be resolved, while Promise.race executes the program using the returned value of the promise that was resolved first. Each promise is not executed sequentially, but parallelly.

var p1 = Promise.resolve(3);
var p2 = 1337;
var p3 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve("foo");
  }, 100);
}); 

Promise.all([p1, p2, p3]).then(values => { 
  console.log(values); // [3, 1337, "foo"] 
});

Promise.race([p1, p2, p3]).then(value => { 
  console.log(value); // 3
});

If all available objects have been resolved, it returns the resolved promise, but if even one object has been rejected, it uses the cause for the first reject and returns a reject promise. Promise.all is explained in greater detail in MDN - Promise.all().

async/await

While promise serves the developer well when dealing with asynchronous operations, there is actually a feature dedicated solely on dealing with asynchronous operations--the async function. async function allows users to deal with multiple promises in synchronous forms familiar to the users. It is possible to have an await keyword preceding a body of a function declared with async. await keyword tells the program to pause executing the async function until the promise is resolved. When the promise is resolved, the program continues to run, and returns the executed value. It is important to note that await only works within the constraints of async functions, and the program will result in syntax errors when await is attempted independently from async. asyns function returns a promise, and when returnValue is returned, it is done so implicitly in the form of Promise.resolve(returnValue). As for errors, try/catch statements from regular functions are also available, and if an error does occur, the error object will be returned as a reject promise.

Promise

function fetchMemberNames(groupId) {
  return getMemberGroup(groupId)
    .then(memberGroup => getMembers(memberGroup))
    .then(members => members.map(({name}) => name))
    .catch(err => {
      showNotify(err.message);
    });
}

fetchMemberNames('gid-11')
  .then(names => {
    if (names) {
      addMembers(names);
    }
  });

async

async function fetchMemberNames(groupId) {
  try {
    const memberGroup = await getMemberGroup(groupId);
    const members = await getMembers(memberGroup);

    return members.map(({name}) => name);
  } catch (err) {
    showNotify(err.message);
  }
}

fetchMemberNames('gid-11')
  .then(members => {
    if (members) {
      addMembers(members);
    }
  });

ES Module

It is also possible to create modules using JavaScript. In ES5, developers had to use bundlers like Webpack, Rollup, and Parcel or use transpiler like Babel in order to make modules compatible on multiple platforms, but ES6 released a new set of simple scripts to create new modules. Every modern browser is now capable of utilizing every module classifications using import and export.

JavaScript module simply means that it is a file with a .js extension. It is no longer necessary to declare modules internally with keywords like module, and JavaScript module codes fundamentally execute on strict. It is also possible for modules to share objects with import and export keywords, but variables and functions declared within the module are only within the module. Then, is there a way to share variables between different modules? The solution is to simply allow export access from other platforms. This method works for functions, classes, variables and etc.

Now, let’s take a look at how to use the export statement. There are two types of exports: the named export, and default export.

Named export

students.js

export const student = 'Park';
export const student2 = 'Ted';

const student3 = 'Abby';
export {student3};

Named export can be called multiple times in a single file. Everything that was exported using the named export must be imported under the same name.

Default export

studentJack.js

export default 'Jack'

On the other hand, default export can only be used once in each file. Also, export default only allows expressions to follow it, and keywords like var, let, and const cannot come after it.

Materials shared using the export can be used in other files and modules. Now, let's take a look at import methods to bring in the exported materials.

Using the Named Exported Objects

import {student, student2, student3} from 'students.js';

console.log(student);   // "Park";
console.log(student2);  // "Ted";
console.log(student3);  // "Abby";

As the example above demonstrates, to import name exported materials, type the names of imported materials surrounded by the curly brackets {...}. How about importing variables and changing their names? It can easily be done by typing as [[desired_new_name]] behind the imported item without the hassle of declaring new variables.

import {student as park, student2 as ted, student3 as abby} from 'students.js';

const student = 'Kim';

console.log(student); // "Kim"
console.log(park);    // "Park"
console.log(ted);     // "Ted"
console.log(abby);    // "Abby"

This method is particularly useful when the imported objects have coinciding names with local variables.

Does this mean that to import objects exported with Named export, it is necessary to type in the names of every single item? Thankfully, the answer is no. It is also possible to import everything by using *.

import * as students from 'students.js';

console.log(students.student);  // "Park"
console.log(students.student2); // "Ted"
console.log(students.student3); // "Abby"

Here, every named export objects are simplified with the * notation, and stored in a desired new variable name by following it up with as [[desired_new_name]].

Importing Objects from Default export

import jack from 'studentJack';

console.log(jack); // "Jack"

The process is similar to Named export, but it does not require the curly brackets {...} and the name of the importing object comes after the import keyword. It is possible to import objects using a different name, but since default export can only be used once per file, as keyword is not required. The variable name used after the import keyword becomes the variable name that stores every data from the default export.

Using Both Methods

When importing, it is possible to use objects exported through both Named export and Default export, simulatenously.To demonstrate, the student.js file will export data, and school.js will import the exported data.

students.js

const jack = 'Jack';

export default jack

const student = 'Park';
const student2 = 'Ted';
const student3 = 'Abby';

export {student, student2, student3};  // Named Export

school.js

import jack {student, student2, student3} from 'students'; //Named import

console.log(jack);    // "Jack"
console.log(student); // "Park"
console.log(student2);// "Ted"
console.log(student3);// "Abby"

Afterword

So far, this guide attempted to share and discover the new features added onto ES6 and how some features have changed using examples. As the ECMAScript standards continue to upgrade, so do the browsers, JavaScript libraries, and tools to assist in the development environment. Author wishes that this guide helps the developers benefit fully from the improved ES6 development environment.


This document is an official Web Front-End development guide written and maintained by NHN Cloud FE Development Lab. Any errors, questions, and points of improvement pertaining to this document should be addressed to the official support channel (dl_javascript@nhn.com).


Last Modified
2019. 03. 29
FE Development LabBack to list