Introduction to Node.js 6 and ES2015
At the time of writing, the latest major releases of Node.js (versions 4, 5, and 6) come with the great addition of increased language support for the new features introduced in the ECMAScript 2015 specification (in short, ES2015, and formerly known also as ES6), which aims to make the JavaScript language even more flexible and enjoyable.
Throughout this book, we will widely adopt some of these new features in the code examples. These concepts are still fresh within the Node.js community so it's worth having a quick look at the most important ES2015-specific features currently supported in Node.js. Our version of reference is Node.js version 6.
Depending on your Node.js version, some of these features will work correctly only when strict mode is enabled. Strict mode can be easily enabled by adding a "use strict"
statement at the very beginning of your script. Notice that the "use strict"
statement is a plain string and that you can either use single or double quotes to declare it. For the sake of brevity, we will not write this line in our code examples, but you should remember to add it to be able to run them correctly.
The following list is not meant to be exhaustive but just an introduction to some of the ES2015 features supported in Node.js, so that you can easily understand all the code examples in the rest of the book.
The let and const keywords
Historically, JavaScript only offered function scope and global scope to control the lifetime and the visibility of a variable. For instance, if you declare a variable inside the body of an if
statement, the variable will be accessible even outside the statement, whether or not the body of the statement has been executed. Let's see it more clearly with an example:
if (false) { var x = "hello"; } console.log(x);
This code will not fail as we might expect and it will just print undefined
in the console. This behavior has been the cause of many bugs and a lot of frustration, and that is the reason why ES2015 introduces the let
keyword to declare variables that respect the block scope. Let's replace var
with let
in our previous example:
if (false) { let x = "hello"; } console.log(x);
This code will raise a ReferenceError: x is not defined
because we are trying to print a variable that has been defined inside another block.
To give a more meaningful example we can use the let
keyword to define a temporary variable to be used as an index for a loop:
for (let i=0; i < 10; i++) { // do something here } console.log(i);
As in the previous example, this code will raise a ReferenceError: i is not defined
error.
This protective behavior introduced with let
allows us to write safer code, because if we accidentally access variables that belong to another scope, we will get an error that will allow us to easily spot the bug and avoid potentially dangerous side effects.
ES2015 introduces also the const
keyword. This keyword allows us to declare constant variables. Let's see a quick example:
const x = 'This will never change'; x = '...';
This code will raise a TypeError: Assignment to constant variable
error because we are trying to change the value of a constant.
Anyway, it's important to underline that const
does not behave in the same way as constant values in many other languages where this keyword allows us to define read-only variables. In fact, in ES2015, const
does not indicate that the assigned value will be constant, but that the binding with the value is constant. To clarify this concept, we can see that with const
in ES2015 it is still possible to do something like this:
const x = {}; x.name = 'John';
When we change a property inside the object we are actually altering the value (the object), but the binding between the variable and the object will not change, so this code will not raise an error. Conversely, if we reassign the full variable, this will change the binding between the variable and its value and raise an error:
x = null; // This will fail
Constants are extremely useful when you want to protect a scalar value from being accidentally changed in your code or, more generically, when you want to protect an assigned variable to be accidentally reassigned to another value somewhere else in your code.
It is becoming best practice to use const
when requiring a module in a script, so that the variable holding the module cannot be accidentally reassigned:
const path = require('path'); // .. do stuff with the path module let path = './some/path'; // this will fail
Note
If you want to create an immutable object, const
is not enough, so you should use ES5's method Object.freeze()
(https://developer.mozilla.org/it/docs/Web/JavaScript/Reference/Global_Objects/Object/freeze) or the deep-freeze
module (https://www.npmjs.com/package/deep-freeze).
The arrow function
One of the most appreciated features introduced by ES2015 is the support for arrow functions. The arrow function is a more concise syntax for defining functions, especially useful when defining a callback. To better understand the advantages of this syntax, let's first see an example of classic filtering on an array:
const numbers = [2, 6, 7, 8, 1]; const even = numbers.filter(function(x) { return x%2 === 0; });
The preceding code can be rewritten as follows using the arrow function syntax:
const numbers = [2, 6, 7, 8, 1]; const even = numbers.filter(x => x%2 === 0);
The filter
function can be defined inline, and the keyword function
is removed, leaving only the list of parameters, which is followed by =>
(the arrow), which in turn is followed by the body of the function. When the list of arguments contains more than one argument, you must surround them with parentheses and separate the argument with commas. Also, when there is no argument you must provide a set of empty parentheses before the arrow: () => {...}
. When the body of the function is just one line, there's no need to write the return
keyword as it is applied implicitly. If we need to add more lines of code to the body of the function, we can wrap them in curly brackets, but beware that in this case return
is not automatically implied, so it needs to be stated explicitly, as in the following example:
const numbers = [2, 6, 7, 8, 1]; const even = numbers.filter(x => { if (x%2 === 0) { console.log(x + ' is even!'); return true; } });
But there is another important feature to know about arrow functions: arrow functions are bound to their lexical scope. This means that inside an arrow function the value of this
is the same as in the parent block. Let's clarify this concept with an example:
function DelayedGreeter(name) { this.name = name; } DelayedGreeter.prototype.greet = function() { setTimeout( function cb() { console.log('Hello ' + this.name); }, 500); }; const greeter = new DelayedGreeter('World'); greeter.greet(); // will print "Hello undefined"
In this code, we are defining a simple greeter
prototype that accepts a name as an argument. Then we are adding the greet
method to the prototype. This function is supposed to print Hello
and the name defined in the current instance 500
milliseconds after it has been called. But this function is broken, because inside the timeout callback function (cb)
, the scope of the function is different from the scope of greet
method and the value of this
is undefined
.
Before Node.js introduced support for arrow functions, to fix this we needed to change the greet
function using bind
, as follows:
DelayedGreeter.prototype.greet = function() { setTimeout( (function cb() { console.log('Hello' + this.name); }).bind(this), 500); };
But since we have now arrow functions and since they are bound to their lexical scope, we can just use an arrow function as a callback to solve the issue:
DelayedGreeter.prototype.greet = function() { setTimeout( () => console.log('Hello' + this.name), 500); };
This is a very handy feature; most of the time it makes our code more concise and straightforward.
Class syntax
ES2015 introduces a new syntax to leverage prototypical inheritance in a way that should sound more familiar to all the developers that come from classic object-oriented languages such as Java or C#. It's important to underline that this new syntax does not change the way objects are managed internally by the JavaScript runtime; they still inherit properties and functions through prototypes and not through classes. While this new alternative syntax can be very handy and readable, as a developer, it is important to understand that it is just syntactic sugar.
Let's see how it works with a trivial example. First of all, let's describe a Person
function using the classic prototype-based approach:
function Person(name, surname, age) { this.name = name; this.surname = surname; this.age = age; } Person.prototype.getFullName = function() { return this.name + '' + this.surname; }; Person.older = function(person1, person2) { return (person1.age >= person2.age) ? person1 : person2; };
As you can see, a person has name
, surname
, and age
. We are providing our prototype with a helper function that allows us to easily get the full name of a person
object and a generic helper function accessible directly from the Person
prototype that returns the older person between two Person
instances given as input.
Let's see now how we can implement the same example using the new handy ES2015 class syntax:
class Person { constructor (name, surname, age) { this.name = name; this.surname = surname; this.age = age; } getFullName () { return this.name + ' ' + this.surname; } static older (person1, person2) { return (person1.age >= person2.age) ? person1 : person2; } }
This syntax is more readable and straightforward to understand. We are explicitly stating what the constructor
is for the class and declaring the function older
as a static
method.
The two implementations are completely interchangeable, but the real killer feature of the new syntax is the possibility of extending the Person
prototype using the extend
and super
keywords. Let's assume we want to create a PersonWithMiddlename
class:
class PersonWithMiddlename extends Person { constructor (name, middlename, surname, age) { super(name, surname, age); this.middlename = middlename; } getFullName () { return this.name + '' + this.middlename + '' + this.surname; } }
What is worth noticing in this third example is that the syntax really resembles what is common in other object-oriented languages. We are declaring the class from which we want to extend, we define a new constructor that can call the parent one using the keyword super
, and we override the getFullName
method to add support for our middle name.
Enhanced object literals
Along with the new class syntax, ES2015 introduced an enhanced object literals syntax. This syntax offers a shorthand to assign variables and functions as members of the object, allows us to define computed member names at creation time, and also handy setter and getter methods.
Let's make all of this clear with some examples:
const x = 22; const y = 17; const obj = { x, y };
obj
will be an object containing the keys x
and y
with the values 22
and 17
, respectively.
We can do the same thing with functions:
module.exports = { square (x) { return x * x; }, cube (x) { return x * x * x; } };
In this case, we are writing a module that exports the functions square
and cube
mapped to properties with the same name. Notice that we don't need to specify the keyword function
.
Let's see in another example how we can use computed property names:
const namespace = '-webkit-'; const style = { [namespace + 'box-sizing'] : 'border-box', [namespace + 'box-shadow'] : '10px10px5px #888888' };
In this case, the resulting object will contain the properties -webkit-box-sizing
and -webkit-box-shadow
.
Let's see now how we can use the new setter and getter syntax by jumping directly to an example:
const person = { name : 'George', surname : 'Boole', get fullname () { return this.name + '' + this.surname; }, set fullname (fullname) { let parts = fullname.split(''); this.name = parts[0]; this.surname = parts[1]; } }; console.log(person.fullname); // "George Boole" console.log(person.fullname = 'Alan Turing'); // "Alan Turing" console.log(person.name); // "Alan"
In this example we are defining three properties, two normal ones, name
and surname
, and a computed fullname
property through the set
and get
syntax. As you can see from the result of the console.log
calls, we can access the computed property as if it was a regular property inside the object for both reading and writing the value. It's worth noticing that the second call to console.log
prints Alan Turing
. This happens because by default every set
function returns the value that is returned by the get
function for the same property, in this case get fullname
.
Map and Set collections
As JavaScript developers, we are used to creating hash maps using plain objects. ES2015 introduces a new prototype called Map
that is specifically designed to leverage hash map collections in a more secure, flexible, and intuitive way. Let's see a quick example:
const profiles = new Map(); profiles.set('twitter', '@adalovelace'); profiles.set('facebook', 'adalovelace'); profiles.set('googleplus', 'ada'); profiles.size; // 3 profiles.has('twitter'); // true profiles.get('twitter'); // "@adalovelace" profiles.has('youtube'); // false profiles.delete('facebook'); profiles.has('facebook'); // false profiles.get('facebook'); // undefined for (const entry of profiles) { console.log(entry); }
As you can see, the Map
prototype offers several handy methods, such as set
, get
, has
, and delete
, and the size
attribute (notice how the latter differs from arrays where we use the attribute length
). We can also iterate through all the entries using the for...of
syntax. Every entry in the loop will be an array containing the key as first element and the value as second element. This interface is very intuitive and self-explanatory.
But what makes maps really interesting is the possibility of using functions and objects as keys of the map, and this is something that is not entirely possible using plain objects, because with objects all the keys are automatically cast to strings. This opens new opportunities; for example, we can build a micro testing framework leveraging this feature:
const tests = new Map(); tests.set(() => 2+2, 4); tests.set(() => 2*2, 4); tests.set(() => 2/2, 1); for (const entry of tests) { console.log((entry[0]() === entry[1]) ? 'PASS' : 'FAIL'); }
As you can see in this last example, we are storing functions as keys and expected results as values. Then we can iterate through our hash map and execute all the functions. It's also worth noticing that when we iterate through the map, all the entries respect the order in which they have been inserted; this is also something that was not always guaranteed with plain objects.
Along with Map
, ES2015 also introduces the Set
prototype. This prototype allows us to easily construct sets, which means lists with unique values:
const s = new Set([0, 1, 2, 3]); s.add(3); // will not be added s.size; // 4 s.delete(0); s.has(0); // false for (const entry of s) { console.log(entry); }
As you can see, in this example the interface is quite similar to the one we have just seen for Map
. We have the methods add
(instead of set
), has
, and delete
and the property size
. We can also iterate through the set and in this case every entry is a value, in our example it will be one of the numbers in the set. Finally, sets can also contain objects and functions as values.
WeakMap and WeakSet collections
ES2015 also defines a "weak" version of the Map
and the Set
prototypes called WeakMap
and WeakSet
.
WeakMap
is quite similar to Map
in terms of interface; however, there are two main differences you should be aware of: there is no way to iterate all over the entries, and it only allows having objects as keys. While this might seem like a limitation, there is a good reason behind it. In fact, the distinctive feature of WeakMap
is that it allows objects used as keys to be garbage collected when the only reference left is inside WeakMap
. This is extremely useful when we are storing some metadata associated with an object that might get deleted during the regular lifetime of the application. Let's see an example:
let obj = {}; const map = new WeakMap(); map.set(obj, {key: "some_value"}); console.log(map.get(obj)); // {key: "some_value"} obj = undefined; // now obj and the associated data in the map // will be cleaned up in the next gc cycle
In this code, we are creating a plain object called obj
. Then we store some metadata for this object in a new WeakMap
called map
. We can access this metadata with the map.get
method. Later, when we cleanup the object by assigning its variable to undefined
, the object will be correctly garbage collected and its metadata removed from the map.
Similar to WeakMap
, WeakSet
is the weak version of Set
: it exposes the same interface of Set
but it only allows storing objects and cannot be iterated. Again, the difference with Set
is that WeakSet
allows objects to be garbage collected when their only reference left is in the weak set:
let obj1= {key: "val1"}; let obj2= {key: "val2"}; const set= new WeakSet([obj1, obj2]); console.log(set.has(obj1)); // true obj1= undefined; // now obj1 will be removed from the set console.log(set.has(obj1)); // false
It's important to understand that WeakMap
and WeakSet
are not better or worse than Map
and Set
, they are simply more suitable for different use cases.
Template literals
ES2015 offers a new alternative and more powerful syntax to define strings: the template
literals. This syntax uses back ticks (`
) as delimiters and offers several benefits compared to regular quoted ('
) or double-quoted ("
) delimited strings. The main benefits are that template literal syntax can interpolate variables or expressions using ${expression}
inside the string (this is the reason why this syntax is called "template") and that a single string can finally be easily written in multiple lines. Let's see a quick example:
const name = "Leonardo"; const interests = ["arts", "architecture", "science", "music", "mathematics"]; const birth = { year : 1452, place : 'Florence' }; const text = `${name} was an Italian polymath interested in many topics such as ${interests.join(', ')}.He was born in ${birth.year} in ${birth.place}.`; console.log(text);
This code will print the following:
Leonardo was an Italian polymath interested in many topics such as arts, architecture, science, music, mathematics. He was born in 1452 in Florence.
Tip
Downloading the example code
Detailed steps to download the code bundle are mentioned in the Preface of this book. Have a look. The code bundle for the book is also hosted on GitHub at:
We also have other code bundles from our rich catalog of books and videos available at:
Other ES2015 features
Another extremely interesting feature added in ES2015 and available since Node.js version 4 is Promise. We will discuss Promise in detail in Chapter 4, Asynchronous Control Flow Patterns with ES2015 and Beyond.
Other interesting ES2015 features introduced in Node.js version 6 are as follows:
- Default function parameters
- Rest parameters
- Spread operator
- Destructuring
new.target
(we will talk about this in Chapter 2, Node.js Essential Patterns)- Proxy (we will talk about this in Chapter 6, Design Patterns)
- Reflect
- Symbols
Note
A more extended and up-to-date list of all the supported ES2015 features is available in the official Node.js documentation: