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.
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.
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.
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.
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.
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.'
}
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.
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.
hello = 'Hello, World!'; // The variable is initialized, but does not cause any errors.
console.log(hello); // 'Hello, World!'
var hello; // Variable is declared
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.
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.
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.
var sum = function(a, b) {
return a + b;
}
const sum = (a, b) => {
return a + b;
};
this
Bindthis
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.
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();
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.
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.
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.
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.
class SomeClass {
//class body
}
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.
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.
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.
In ES6, if the key
text and the value
variable are identical, developers only have to type them once.
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
};
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.
ES6 also allows users to omit the function
keyword and the colon :
after the method name when defining a method within an object.
var dog = {
name: 'Lycos',
bark: function () {
console.log('Woof! Woof!')
}
};
dog.bark(); // 'Woof! Woof!';
const dog = {
name: 'Lycos',
bark() {
console.log('Woof! Woof!')
}
};
dog.bark(); // 'Woof! Woof!';
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.
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!';
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!'
}
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.
var brandName = 'TOAST';
var productName = 'UI';
console.log('Hello ' + brandName + ' ' + productName + '!'); // 'Hello TOAST UI!';
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.
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.';
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.';
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!
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.
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.
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);
}
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.
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.
function printError(error) {
var errorCode = error.errorCode;
var msg = error.errorMessage;
console.log('Error Code: ' + errorCode);
console.log('Message: ' + msg);
}
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 :
.
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.
function sayName(name) {
if (!name) {
name = 'World';
}
console.log('Hello, ' + name + '!');
}
sayName(); // "Hello, World!"
const sayName = (name = 'World') => {
console.log(`Hello, ${name}!`);
}
sayName(); // "Hello, World!"
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.
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
});
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
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.
ES6 massively upgraded the accessibility of object literals and array literals. Rest parameters and spread operators are such cases.
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.
Using spread operator, an array can easily be expanded into a list of parameters.
var friends = ['Jack', 'Jill', 'Tom'];
textToFriends(friends[0], friends[1], friends[2]);
const friends = ['Jack', 'Jill', 'Tom'];
textToFriends(...friends);
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.
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'
};
const friends = ['Jack', 'Jill', 'Tom'];
const moreFriends = [...friends, 'Kim'];
const drinks = {
coffee: 'coffee',
juice: 'orange juice'
};
const newDrinks = {
...drinks,
juice: 'tomato juice',
water: 'water'
};
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.
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);
}
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.
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.
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 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.
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
.
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.
doSomething(function(result) {
doSomethingElse(result, function(newResult) {
doThirdThing(newResult, function(finalResult) {
console.log('Got the final result: ' + finalResult);
}, failureCallback);
}, failureCallback);
}, failureCallback);
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.
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().
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.
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 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);
}
});
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.
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 import
ed under the same name.
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.
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]]
.
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.
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"
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 |