Quick Introduction to ES6

Gene Kuo
9 min readOct 12, 2019

In this article, I summarises several ES6 features and a brief introduction to the concept and usage of these features with simple examples. The main goal is to provide newcomers a quick start in making use of ES6.

let and const

Prior to ES6, we can use var to declare a variable inside a code block and the JavaScript engine hoists the declaration to the top of the execution context, such as a function. This mechanism makes the variable visible outside of the block in which it is declared. It sometimes causes confusion if there are more than one variables of the same name declared inside and outside the function.

var person = "John";
(function () {
console.log("Person inside is " + person);
var person = "Tom";
})();
console.log("Person outside is " + person);

We can comment out var person = “Tom”; and the console will print out person inside is John person outside is John. However, if we uncomment it, the console will print out person inside is undefined person outside is John. Hoisting also applies to function declaration in which a function can be invoked before it is declared. However, a function expression is not hoisted since it is considered variable initialization.

doSomething(); // TypeError: doSomething is not a function
var doSomething = function() {
console.log("do something");
}

let and const remove this confusion by using block scoping. let allows you declare a variable and initialize with a value, then the variable can be reassigned to another value. const can be used to initialize a variable with a value only once, however, its properties can be changed. It can not be reassigned.

const users = [
{ id: 1, name: 'John' },
{ id: 2, name: 'Mary' }
];
users[0].name = "Tom";
console.log(users);

The following listing is an example of block scoping with let. So there is no hoisting confusion.

let person = "John";
(function () {
console.log("Person inside function is " + person);
if(true) {
let person = "Tom";
console.log("Person inside block is " + person);
}
})();
console.log("Person global is " + person);

Template literals

Template literals are string surrounded by backticks (`)and allow us to write expressions inside the template literals. Strings can also span multiple lines using template literals.

const user = "John";
console.log(`the user is ${user}`);
function getUser() {
return "John";
}
console.log(`the user is ${getUser()}`);

Tagged template strings

If a function name tags (precedes) a template string, the string is evaluated first and is passed to the function. The string part of the template literal will be passed as an array to the function.

function greeting(stringParts, firstName, lastName) {
console.log(stringParts);
console.log(firstName);
console.log(lastName);
}
const firstName = "John";
const lastName = "Tom";
greeting`Hello, ${firstName} ${lastName}!`

Optional parameters and default values

We can specify default values as arguments to a function during the invocation of the function if no value is provided.

function doSomething(work, place = "home") {
console.log(`${work} at ${place}`);
}
doSomething("reading");
doSomething("writing", undefined);
doSomething("reading", "library");

Arrow function expressions

Arrow function expressions represent a short notation for anonymous functions. The following listing gives examples of arrow function expressions.

let add = (arg1, arg2) => arg1 + arg2; // the result is returned(arg1, arg2) => {
// do something
return res; // multi-line arrow function with explicitly return
}
() => { // without argument
// do something
return res;
}
arg1 => { // single argument, no parentheses needed
// do something
}

In addition, the following listing illustrates passing arrow function expressions to JavaScript array’s methods to operate on provided arrays.

const arr = [1,2,3,4,5];
console.log("Sum is " + arr.reduce((a, b) => a+b));
console.log("Even numbers are " + arr.filter(val => val % 2 === 0));

The this variable can have different values depending on how the function is called and on the use of strict mode. Array function expressions also add lexical scope for the this variable. Prior to ES6, the following constructor function sometimes confuses developers for this pointing the wrong object.

function Greeting(message) {
this.message = message;
console.log(`this.message: ${this.message}`);
setInterval( function () {
console.log(`greeting: ${this.message}`);
}, 1000); // this.message is undefined if strict mode
}
const greeting = new Greeting("Hello world");

We can replace the anonymous function above with the arrow function expression in the following listing to fix it.

function Greeting(message) {
this.message = message;
console.log(`this.message: ${this.message}`);
setInterval(() => console.log(`greeting: ${this.message}`)
, 1000);
}
const greeting = new Greeting("Hello world");

The rest operator

We can use the rest operator for a variable number of parameters in a function. The rest operator (…) must be the last parameter in the function signature. The function can handle the rest arguments as an array when it is invoked.

function placeOrder(customer, ...products) {
console.log(`Place an order for a customer: ${customer}`);
products.forEach( (product) =>
console.log(`Add ${product} to order`));
}
placeOrder("John", "iPhone", "iPad", "TV");

The spread operator

The spread operator is represented the same as the rest operator (…) but turns an array into a list of values or function arguments. In the following listing, the spread operator extracts each element of an array, and then creates a new array from those elements or processes those elements.

let array1 = [1,2,3,4,5];
let array2 = [...array1];
console.log(array2);
array1.push(...array2);
console.log(array1);
const max = Math.max(...array2);
console.log(max);

We often want to manage the state of an application by cloning the original state object with modification of some properties instead of directly mutating the original state object. This immutability is the best practice when managing the state of application in response to events triggered by users or systems.

const user = { id: 1, lastName: "John" };
const clonedUser = {...user};
const clonedAndModifiedUser = {...user, lastName: "Tom"};
console.log(`original: ${user.lastName}`);
console.log(`cloned: ${clonedUser.lastName}`);
console.log(`cloned and modified: ${clonedAndModifiedUser.lastName}`);

We have to note that cloning with spread operator create a shadow copy of the object. If an object is cloned and some of its properties are also objects, only the reference to the nested objects are cloned. If some properties of the referenced objects changes, the clone also changes in the same way.

Destructuring

Destructuring extracts the structure or takes parts from an object or an array by specifying a matching pattern. Starting from ES2018, we can also use the syntax similar to rest and spread operator when destructuring objects.

function getUser() {
return {
firstName: "John",
lastName: "Tom",
supervisor: {
name: "Mary"
}
};
}
let { lastName, supervisor: {name}} = getUser();
console.log(`${lastName}`);
console.log(`${name}`);
let { firstName, ...otherInfo } = getUser();
console.log(`${firstName}`);
console.log(`${otherInfo}`);

We can destructuring arrays by specifying variables that match array’s indexes.

let [ name1, name2, name3 ] = [ "John", "Tom", "Mary" ];
let [ name4, , name5] = [ "John", "Tom", "Mary" ];
let [ name6, ...others] = [ "John", "Tom", "Mary" ];
console.log(`name1: ${name1}, name2: ${name2}, name3: ${name3}`);
console.log(`name4: ${name4}, name5: ${name5}`);
console.log(`name6: ${name6}, others: ${others}`);
function printUsers([ first, ...others ]) {
console.log(`first: ${first}`);
console.log(`others: ${others}`);
}
printUsers([ "John", "Tom", "Mary" ]);

Classes

In ES5, we can use a mechanism called prototypal inheritance to create objects and inheriting from their ancestors.

// constructor function
function Person(name) {
this.name = name;
}
// declare additional method on its prototype
Person.prototype.getName = function() {
return this.name;
}
// constructor function
function Employee(name, position) {
// call Person function with employee's "this" and name
Person.call(this, name);
this.position= position;
}
// implements inheritance by pointing at its ancestor
Employee.prototype = new Person();
// Add Employee's specific method
Employee.prototype.getPosition = function() {
return this.position;
}
var employee = new Employee("John", "developer");
console.log(`Employee name: ${employee.getName()}, Position: ${employee.getPosition()}`);

ES6 introduced class and extends to give syntactic sugar to simulate inheritance in traditional object-oriented programming languages such as Java and C#. Even though JavaScript doesn’t allow us to declare member variables, but we can declare static variables and methods that apply to a class as a whole instead of instances.

constructor is used to denote a special function that will be called once when the object is created. Within the constructor function, we can use this which points to the current object to initialize the object’s properties.

When a subclass defines its own constructor, within the constructor, we must call super() to invoke superclass’s constructor before accessing this object or returning from the constructor. super keyword can also be used to implement method-overriding similar to some traditional object-oriented languages.

class Person {
static counter = 0;
constructor(name) {
this.name = name;
Person.counter++
}
getName() {
return this.name;
}
static getCounter() {
return Person.counter;
}
}
class Employee extends Person {
constructor(name, position) {
super(name);
this.position = position;
}
getName() {
return super.getName();
}
getPosition() {
return this.position;
}
}
let employee1 = new Employee("John", "developer");
console.log(`Employee1 name: ${employee1.getName()}, Position: ${employee1.getPosition()}`);
let employee2 = new Employee("Tom", "manager");
console.log(`Employee2 name: ${employee2.getName()}, Position: ${employee2.getPosition()}`);
console.log(`Total employees: ${Employee.getCounter()}`);

Modules

ES6 modules are utilized to organize the application into logical and reusable units, and to expose APIs from the modules. Prior to ES6, there are multiple modules standards for implementations, such as CommonJS standard for apps running outside browsers and AMD standard for apps running inside browsers. Modules also help developers define dependencies between code and load them into the execution environment on demand. In ES6, a script is a module if it uses import and/or export.

The mechanism of ES6 modules can prevent us from polluting global scope with variables. It also help us to control access to our scripts and their members (classes, functions, and variables), which makes our code more manageable and maintainable.

There are named export and anonymous export. The usage is simple and is listed in the following.

// in funcs.js
export function bar() { // named export
console.log("bar is invoked");
}
export function foo() {
console.log("foo is invoked");
}
function baz() { // private function
}
export default function {} // anonymous export// in index.js
// destructuring and give a name to the anonymous exported function
import doSomthing, {foo, bar} from 'funcs';
foo();
doSomthing();

All web browsers support modules in the <script> tag and load the script as ES6 modules.

<script type="module", src="./app.js"></script>

Promises

JavaScript promises allow you to avoid nested callback calls and make the async code more readable. The promise object represents the future result of an asynchronous operation, which can be in one of the following states: fulfilled, rejected, and pending. We can instantiate a Promise object using its constructor with a function that has resolve and reject functions as its arguments.

function getProducts() {
return new Promise(
function(resolve, reject) {
console.log("Get products");
// timeout 1 sec to simulate an async call
setTimeout(function() {
const success = true;
if (success) {
resolve("Success");
} else {
reject("Fail");
}
}, 1000);
}
);
}
getProducts()
.then((prod) => console.log(prod))
.catch((err) => console.error(err));
// the following line will print first
console.log("Waiting for getting products...");

In the above example, after one second, resolve(“Success”) is called from inside the function. It results in the invocation of then() and receives “Success” as its argument.

We can also design an ordered asynchronous operations by chaining promises.

function getUser() {
return new Promise(
function (resolve, reject) {
console.log("Getting user");
setTimeout(function() {
const success = true;
if (success) {
resolve("user123");
} else {
reject("Failed");
}
}, 1000);
}
);
}
function getRoles(user) {
return new Promise(
function (resolve, reject) {
console.log("Getting roles");
setTimeout(function() {
const success = true;
if (success) {
resolve(`Found the roles for ${user}`);
} else {
reject("Failed");
}
}, 1000);
}
);
}
getUser()
.then((user) => {
console.log(user);
return user;
})
.then((user) => getRoles(user))
.then((role) => console.log(role))
.catch((err) => console.error(err));
console.log("Getting user and then getting roles");

If there are multiple asynchronous operations that don’t depend on each other, we can use Promise object’s all() method that takes those promises and resolves all of them. The all() method also returns a Promise object which we can apply then() and/or catch() to the results.

Promise.all([ getProducts(),
getUser()])
.then((results) => console.log("All resolved"))
.catch((err) => console.log(err));

ES8 (ES2017) : async and await

async/await allows you to treat functions returning promises as if they are synchronous. async marks an asynchronous function and await marks the invocation of an async function. With await, the operation will not proceed to the next one until the invocation of an async function returns the result or throws an error.

(async function getUserRoles() {
try {
const user = await getUser();
console.log(`Found ${user}`);
const roles = await getRoles(user);
console.log(roles);
} catch(err) {
console.error(err);
}
})();
console.log("Chained getUser and getRoles");

Conclusions

There are more features in ES6 and beyond, and they are continuously improving. They are also some deep mechanisms between older JavaScript and ES6 which I purposely leave out of this article, to focus on the introduction and simple usage of ES6, instead. Hope you enjoy it. Thanks.

--

--

Gene Kuo

Solutions Architect, AWS CSAA/CDA: microservices, kubernetes, algorithms, Java, Rust, Golang, React, JavaScript…