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
andconst
- Arrow Functions
- Template Literals
- Spread Operator
- Rest Parameters
- Destructuring Assignment
- Enhanced Object Literals
for..of
Iterator- New Array and String Methods
- Classes
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:
- A
const
-declared variable must be assigned a value when created. - 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:
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, In ES5, you still have to use to ...
, will expand the array into a comma separated list to the Math.max()
function, which is a parameter format accepted by the function..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
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;
}
}