Writing Better JavaScript with ES6

ES6, or formally ECMAScript2015, is the latest version of JavaScript and has new major enhancements for a more expressive syntax and fixes to old bad parts of the language. In this post, I'll explore some of the new features that can help improve and simplify building complex applications in JavaScript. But first, let's look at a brief history of the language.

A Brief History of JavaScript

1995 - Brendan Eich created first version of JavaScript (originally named Mocha)

1997 - ECMAScript standard established

1999 - ES3: Introduction of try/catch, Errors

2005 - Introduction of Ajax

2009 - Introduction of ES5, the first major standardized release after ES3 and the current implementation of most modern browsers. This version include features such as -- struct mode, native JSON support, foreach, map, keys, etc.

2015 - ES6/ECMAScript2015 released as newest standard

With that latest standard, many of the quirks have been addressed and new enhancements were introduced such as lexical scoping, native promise suport, arrow functions, template strings -- all new features that will help improve writing better applications.

The following are what I consider as the most useful features of ES6.

Block scope with let and const

Scope defines where your declared variables are available in your program. In most languages, variables are only available in enclosing blocks (usually in enclosing braces, but in JavaScript, this is not the case. The lifetime of variables in JavaScript are enclosed inside a function. Consider the following function:

function scopeTest() {  
  var myVar = "I'm defined";
  function printVar() {
    console.log(myVar);
  }
  printVar();
}

scopeTest();  
// logs - I'm defined

console.log(myVar);  
// ReferenceError: myVar is not defined

In JavaScript, variables and function declarations are hoisted, or lifted, to the top of a function. And this means that a variable or a function declaration isn't declared where you think it is. Scoping of variables in JavaScript is one of the most common sources of confusion. And this is the reason why it is best practice to always define variables at the top of a function.

With the introduction of let and const, declaring variables are no longer hoisted to the top of a function, and we can now declare scopes of variables inside blocks. Using them will make your code easier to read and less confusing. It will also make it easy to limit the visibility of variables.

The const are let keywords are very similar but with two main differences:

  1. A const-declared variable must be assigned a value when created.

  2. A const-declared variable is immutable. You cannot change the value after it has been created.

Using let in a for loop keeps the counter variable local to the loop.

for (let i = 0; i < 10; i++) {  
  console.log(i);
}
console.log(i); // Uncaught ReferenceError: i is not defined  

As a general rule, always prefer const for all variables that will never change. Otherwise, use let for variables that values do change. And finally, avoid var at all costs.

Arrow Functions

This new feature is one of the most exciting improvements in ES6. This new syntax for writing functions allows you to write functions in an entirely new way -- much shorter and inline, which is very useful in functional programming.

Arrow functions also fix the annoyances from the this keyword, it makes this behave properly. In ES5, functions always define their own this upon invocation, which causes this to refer to a different object. In most cases, what we want is for this to refer to the same object whether it is in an outer function or an inner function. Let's look at the following example.

const user = {  
    username : "lemmy",
    roles : ["Admin", "Manager", "Contributor", "Moderator", "User"],
    showRole : function () {
        // this.username is "lemmy"
        // const self = this; 
        this.roles.forEach(function(role) {
            // this.username is undefined, so we use the const "self" variable
            console.log(`${this.username}'s role - ${role}`);
        });
    }
}

user.showRole();  

In this example, the showRole will result in an undefined for this.username because the function defined this inside the function, showRole. Javascript functions always points to the object referred to by this where it was invoked (also referred as the call-site), and not where it was defined. There are four rules to determine which object this refers to:

  1. Default Binding
  2. Implicit Binding
  3. Explicit Binding
  4. new Binding

Describing the four rules is a lengthly subject and deserves a separate post, but for now, you can read more about them here. Just keep in mind that this always refers to an object no matter what, it will never refer to a primitive value such as a boolean, number, or string.

In ES6, using arrow functions will no longer bind a this value. Refactoring the above example, it becomes:

const user = {  
    username : "lemmy",
    roles: ["Admin", "Moderator", "Reporting", "User"],
    showRole: function () {
        this.roles.forEach(role => {
            console.log(`${this.username}'s role - ${role}`)
        })
    }
}
user.showRole()  

With arrow functions, the big difference is the new behavior of this, and it is now bound to the enclosing scope at creation time and it can no longer be changed. Even if you use bind, call, or apply will have no effect on this.

Arrow functions have a number of syntax variations:

let roles =  ["Admin", "Moderator", "Reporting", "User"]

let rolesUpperCase = roles.map((role) => {  
    return role.toUpperCase()
});

// 2. In a single-line arrow function, curly braces and return statements
// are optional. The function implicitly returns the value of the last expression.
rolesUpperCase = roles.map((role) => role.toUpperCase())

// 3. Optional parentheses in argument if there's only one parameter
rolesUpperCase = roles.map(role =>  role.toUpperCase())

// 4. If your function takes multiple arguments, you must enclose them in parentheses.
rolesUpperCase = roles.map((role, unusedParam) => role.toUpperCase())  

Arrow functions are also very useful for short callbacks that only return results of expressions. In ES5, such callbacks are quite verbose:

// ES5
const arr = [1, 2, 3];  
let squares = arr.map(function (x) { return x * x })  

In ES6, the same function can be written shorter:

// ES6
const arr = [1, 2, 3];  
let squares = arr.map((x) => x * x )  

As you can see, this new syntax is very ideal in functional programming, where you use higher-order functions that accept other functions (callback functions), as arguments. Having the ability to write very short and inline callback functions allows you to write more expressive functions.

Template Literals

Concatenating strings in ES5 requires using the + operator. In ES6, you can now use the new ${} syntax as a template placeholder, which will be replaced by the actual value at runtime.

This new syntax allows you to write multi-line strings that are easier to read and write. Here are a few examples;

// Basic literal string
`This is a simple template string.`

// Multiline string
`This is not legal
is ES5, but okay in ES6`

// Interpolate variable bindings
const name = 'Karlo', role = "admin";  
`Hello ${name}, your current role is ${admin}`

// Unescape template strings
String.raw`In ES5 "\n" is a new line.`  

Spread Operator

The spread operator in ES6 basically spreads out the elements from an array. For example,

const arr1 = [5, 10, 15, 20];  
const arr2 = [30, 40, 50, 60];  
const combinedArr  = [...arr1, ...arr2]  
console.log(combinedArr); // [5, 10, 15, 20, 30, 40, 50, 60]  

This tool is very useful when you want to transform an array into a list of comma separated list. Consider the following example:

const num = [40, 30, 13, 15, 22];  
const max = Math.max(...num);  
console.log(max); //40  

The spread operator, ..., will expand the array into a comma separated list to the Math.max() function, which is a parameter format accepted by the function.

In ES5, you still have to use to .apply() method first before we can apply the max() function:

const num = [40, 30, 13, 15, 22];  
const max = Math.max.apply(null, num);  
console.log(max); // 40  

The first parameter of apply is the value we like to assign the this value so we can call the max function. As you can see, this syntax is a little confusing and not so easy to read.

Rest Parameters

The spread operator in ES6 can also gather parameters passed to function.

const sum = function() {  
  const numbers = Array.prototype.slice.call(arguments);
    return numbers.reduce((a,b) => a + b);
};
sum(1, 2, 3, 4, 5);  

Before ES6, you still need to use the call() method before you can use the slice method just to create an array of arguments. With rest parameters in ES6, the same function above can be simplified as:

const sum = function(...numbers) {  
  return numbers.reduce((a, b) => a + b);
};
sum(1,2,3,4,5); // 15  

You can also combine rest parameters and the spread operator.

const product = (multiplier, ...numbers) => {  
  return numbers.map(n => n * multiplier);
}
product(10,2,3,4,5); // [20, 30, 40, 50]  

Rest parameters makes your code easier to read since you can easily tell a function that has a variable number of parameters at a glance.

Destructuring Assignment

Destructuring assignment in ES6 is a convenient way of extracting data from objects and arrays into distinct variables.

let x, y, rest;  
[x, y] = [1, 2];
console.log(x); // 1  
console.log(y); // 2

({a, b} = {a: 1, b: 2})
console.log(a); // 1  
console.log(b); // 2  

Enhanced Object Literals

Here's a very common JavaScript function:

function createUser(firstName, lastName) {  
  return {
    firstname: firstName,
    lastName: lastName
  }
}

When you call the createUser function, it creates an object literal with the keys of firstName and lastName.

With ES6, you can now remove the keys, and just write it as:

function createUser(firstName, lastName) {  
  return {
    firstname,
    lastName
  }
}

This is a much simpler way of writing new objects.

for..of Iterator

The most common way to iterate over an array is to use the for loop statement, which also allows you to access the index of each item:

var arr = [1,2,3];  
for (var i=0; i<=arr.length; i++) {  
  console.log(arr[i]);
}

There's also another option to use forEach() statement, which is shorter to write:

arr.forEach(function(elem) {  
    console.log(elem);
});

Each of these methods has a particular use case. The first method has the advantage of having the ability to break inside the loop, while the latter is more concise.

In ES6, there's a new loop syntax - for..of, which has the advantages of both methods.

const arr = [2,4,6];  
for (const elem of arr) {  
  console.log(elem);
}

The new syntax also allows you to access the index of each element using the the new entries() array method:

const arr = [5,10,15];  
for (const [index, elem] of arr.entries()) {  
  console.log(`${index} - ${elem}`);
}

New Array and String Methods

ES6 added several new methods for strings, arrays, and numbers. Here are a few methods that I find very useful:

if (str.startsWith('M')) {}  
if (str.endsWith('M')) {}  
if (str.includes('M')) {}  
'6'.repeat(3) // produces 666

Array.from(document.querySelectorAll("*")) // Returns a real Array  
Array.of(1, 2, 3) // Similar to new Array(...), but without special one-argument behavior  
[0, 0, 0].fill(6, 1) // [0,6,6], fills all elements of array from start index to end with a static value
[1,2,3].findIndex(x => x == 2) // 1
["a", "b", "c"].entries() // iterator [0, "a"], [1,"b"], [2,"c"]
["a", "b", "c"].keys() // iterator 0, 1, 2
["a", "b", "c"].values() // iterator "a", "b", "c"

Classes

ES6 classes are not traditional classes that you would find in other Object-oriented programming languages. They are just syntactic sugar over the prototype-based object-oriented pattern in JavaScript.

In ES5, writing constructor functions is quite verbose to write:

function User(username) {  
  this.username = username;
}
User.prototype.diplayUser = function() {  
  return this.username
};

In ES6, it is much simpler:

class User {  
  constructor(username) {
    this.username = username;
  }
  displayUser() {
    return this.username;
  }
}

Subclassing in ES5 is also quite complicated. For example:

function Moderator(username, role) {  
    User.call(this, username); // super(username)
    this.role = role;
}
Moderator.prototype = Object.create(User.prototype);  
Moderator.prototype.constructor = User;  
Moderator.prototype.displayUser = function () {  
    // super.displayUsername()
    return User.prototype.displayUser.call(this)
           + ' (' + this.role + ')';
};

But in ES6, it is now easier with the extends keyword:

class Moderator extends User {  
  constructor(username, role) {
    super(username);
    this.role = role;
  }
  displayUser() {
    return super.displayUser() + '-' + this.role;
  }
}

Summary

These are just some of the most useful new features of ES6, and you can already start using them in building your applications. You will need to use a transpiler such as Babel or the latest version of Node if your development environment still don't ES6. There are other noteworthy features which were not covered here, but the features that were covered are the most useful in my experience. To learn more about the other new features, you can check them out in ES6 Features website.

When starting your new ES6 project, what's important to remember is that ES6 is mostly syntactic sugar over ES5, and hence, it's backward compatible. This means you can convert your ES6 code automatically to ES5 without much hassle. Consider these new features as new tools that will enable you to write better and succinct code in JavaScript.

Further Reading