Search icon CANCEL
Arrow left icon
Explore Products
Best Sellers
New Releases
Books
Videos
Audiobooks
Learning Hub
Conferences
Free Learning
Arrow right icon
Arrow up icon
GO TO TOP
Node.js  Design Patterns

You're reading from   Node.js Design Patterns Master best practices to build modular and scalable server-side web applications

Arrow left icon
Product type Paperback
Published in Jul 2016
Publisher Packt
ISBN-13 9781785885587
Length 526 pages
Edition 2nd Edition
Languages
Tools
Arrow right icon
Authors (3):
Arrow left icon
Luciano Mammino Luciano Mammino
Author Profile Icon Luciano Mammino
Luciano Mammino
Mario Casciaro Mario Casciaro
Author Profile Icon Mario Casciaro
Mario Casciaro
Joel Purra Joel Purra
Author Profile Icon Joel Purra
Joel Purra
Arrow right icon
View More author details
Toc

Table of Contents (12) Chapters Close

Preface 1. Welcome to the Node.js Platform 2. Node.js Essential Patterns FREE CHAPTER 3. Asynchronous Control Flow Patterns with Callbacks 4. Asynchronous Control Flow Patterns with ES2015 and Beyond 5. Coding with Streams 6. Design Patterns 7. Wiring Modules 8. Universal JavaScript for Web Applications 9. Advanced Asynchronous Recipes 10. Scalability and Architectural Patterns 11. Messaging and Integration Patterns

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:

http://bit.ly/node_book_code.

We also have other code bundles from our rich catalog of books and videos available at:

https://github.com/PacktPublishing/.

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:

https://nodejs.org/en/docs/es6/.

lock icon The rest of the chapter is locked
Register for a free Packt account to unlock a world of extra content!
A free Packt account unlocks extra newsletters, articles, discounted offers, and much more. Start advancing your knowledge today.
Unlock this book and the full library FREE for 7 days
Get unlimited access to 7000+ expert-authored eBooks and videos courses covering every tech area you can think of
Renews at €18.99/month. Cancel anytime