ES6 and Beyond - Part 1

by Paul Selden — on  , 

cover-image

This is a series of posts that will help familiarize you with many of the features of the recently-approved ECMAScript 6 specification (aka ES2015, ES Harmony, ESNext) as well as go through some of the proposed changes for ES6+/ES7. It will focus on providing examples of how your code changes before and after applying the new ES6 and beyond features.

Note: ECMAScript is the “official” name of the JavaScript language specification. All browsers updated in the past few years support the previous version (ES5), and the latest versions of many browsers support much (but not all) of ES6. When using ES6 code in production, it is recommended that you first pass it through a transpiler such as Babel which will turn your ES6 code into ES5 code for maximum browser support. See this compatibility table for more information about which environments support ES6 features natively.

Part 1 - Sugar

This post will focus on the sweeter side of ES6 – the syntactic sugar. These are features that can be written in ES5 without the addition of helper libraries, but have new syntax in ES6 that can often make your code shorter and clearer.

Template Strings

Template strings a new type of string literal which eases the creation of multi-line strings, allow for interpolation inside of the string and, ends once and for all the “single quote or double quote debate”. Template strings use the backtick ( ` ) operator to start and close the string.

Multi Line Strings

If you’ve ever had to build an HTML in your JavaScript then you’ve encountered the problem of not having “real” multi-line strings available at your disposal. You’ve either had to use string concatenation with + or added them to an array and joined them at the end. Template strings solve this problem by allowing you to use multi-line strings.

Before

1
2
3
4
5
6
7
function template() {
  return '<ul id="myList"' + 
             'class="list">' +
            '<li>First Item</li>' +
            '<li>Second Item</li>' + 
         '<div>';
}

After

1
2
3
4
5
6
7
function template() {
  return `<ul id="myList"
              class="list">
            <li>First Item</li>
            <li>Second Item</li>
          <div>`;
}

Single and Double Quotes

Template strings allow you to use both single and double quotes without having to escape them. The bad news is that if you need a backtick in your template strings you still will need to escape them, but that should happen much less frequently.

Before

1
var sentence = "I've got one word for template strings: \"Great\"";

After

1
var sentence = `I've got one word for template strings: "Great"`;

Interpolation

Perhaps the best new feature of template strings is the ability to perform interpolation. Similar to other languages that support interpolation, JavaScript template strings allow you to capture the result of an expression directly in your string without having to rely on manual concatenation or string replaces. By wrapping the expression in your template string inside a ${} you mark what things are to be replaced in your string.

Before

1
2
3
4
5
6
7
function greet(name) {
  return 'Welcome, ' + name + '! The time is ' + getTime() + '.';
}

function getTime() {
  return new Date();
}

After

1
2
3
4
5
6
7
function greet(name) {
  return `Welcome ${name}! The time is ${getTime()}`;
}

function getTime() {
  return new Date();
}

Function Enhancements

ES6 has several features that eliminate the boilerplate needed for some common tasks when creating/calling functions: default parameters, rest parameters, and spread operators.

Default Parameters

Often times you’ll create a function that accepts multiple parameters, some of which are optional and have defaults. The non-ES6 way of doing this would be to check if the argument is undefined and then set the value in that case. In ES6 this is added directly to the language in ES6 in the function definition. ES6 default parameters are evaluated at call time, so you can put any expression in them, including method calls and they will be evaluated when the function is called. Note that default parameters in ES6 do not extend to the inner contents of objects – if an object is passed to a function with a default parameter of an object, it will not “merge” in the inner values of that default.

Before

1
2
3
4
5
6
7
8
9
10
11
function getDefaultAccountId() {
  return 42;
}

function getAccount(accountId, includeUser, options) {
  if(accountId === undefined) accountId = getDefaultAccountId();
  if(includeUser === undefined) includeUser = true;
  if(options === undefined) options = { timeout: 30000 };
  
  // logic to get the account
}

After

1
2
3
4
5
6
7
function getDefaultAccountId() {
  return 42;
}

function getAccount(accountId = getDefaultAccountId(), includeUser = true, options = { timeout: 30000 }) {
  // logic to get the account
}

Rest Parameters

Rest parameters allow you to represent a variable number of arguments to a function call as an Array. The ES5 way of achieving this is by using the arguments object and mapping the contents to a real Array. By using rest parameters you can be more explicit about the expected function parameters as well as avoid the boilerplate conversion of arguments into an Array.

Before

1
2
3
4
5
6
7
8
9
10
11
12
function getUsers(userType) {
  var userIds = Array.prototype.splice.call(arguments, getUsers.length); // convert arguments to a "real" Array.
  return userIds.map(function (userId) {
     return getUser(userType, userId);
  });
}

function getUsers(userType, userId) {
  // a function that gets the user based on userType and user id 
}

getUsers('ADMIN', 1, 2, 3);

After

1
2
3
4
5
6
7
8
9
10
11
function getUsers(userType, ...userIds) {
  return userIds.map(function (userId) {
     return getUser(userType, userId);
  });
}

function getUsers(userType, userId) {
  // a function that gets the user based on userType and user id 
}

getUsers('ADMIN', 1, 2, 3);

Spread Operators

Spread operators can kind of be considered the inverse of rest parameters – instead of gathering multiple parameters into a single array it spreads the contents of a single array into multiple parameters. In many places where you previously used “apply” you can use the spread operator instead.

One common use is when you want to push several items on to the end of an existing array.

Before

1
2
3
4
5
6
7
8
// pushes all items from the newItems array onto the items list
function pushAll(items, newItems) {
  items.push.apply(items, newItems);
}

var items = [1, 2, 3];
var otherItems = [4, 5, 6];
pushAll(items, otherItems); // items: [1, 2, 3, 4, 5, 6];

After

1
2
3
4
5
6
7
8
// pushes all items from the newItems array onto the items list
function pushAll(items, newItems) {
  items.push(...newItems);
}

var items = [1, 2, 3];
var otherItems = [4, 5, 6];
pushAll(items, otherItems); // items: [1, 2, 3, 4, 5, 6];

Because apply cannot be used with constructor functions, the spread operator is perfect for them.

Before

1
2
3
4
5
6
7
8
9
10
11
12
function constructDate(dateArray) {
  if(dateArray.length === 3) { // [1999, 11, 31]
    return new Date(dateArray[0], dateArray[1], dateArray[2]);
  } else if(dateArray.length === 4) {
    return new Date(dateArray[0], dateArray[1], dateArray[2], dateArray[3]);
  } else if(dateArray.length === 5) {
    return new Date(dateArray[0], dateArray[1], dateArray[2], dateArray[3], dateArray[4]);
  } // etc...
}

var dateArray = getDate(); // gets a date as an array
var date = constructDate(dateArray);

After

1
2
var dateArray = getDate(); // gets a date as an array
var date = new Date(...dateArray);

Destructuring Assignment

ES6 introduces a type of shorthand called destructuring, which uses pattern matching to assign the contents of an array or object into individual variables. The left hand side of the expression is matched against the contents of the right hand side of the expression. This can reduce the number of statements you have to write in order to pull values out of an object or array. In many cases this eliminates the need to make a temporary variable in order to extract variables from the result of a method call.

Array Destructuring

Array destructuring is used to pull variables out of an array based on their position within the array.

Before

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// parses a date in the form of "YYYY-MM-DD hh:mm:ss"
function parseDate(dateStr) {
  var parts = dateStr.split(' ');
  
  var date = parts[0];
  var dateParts = date.split('-').map(Number);
  var year = dateParts[0];
  var month = dateParts[1];
  var day = dateParts[2];
  
  var time = parts[1];
  var timeParts = time.split(':').map(Number);
  var hours = timeParts[0];
  var mins = timeParts[1];
  var seconds = timeParts[2];
  return new Date(year, month - 1, day, hours, mins, seconds);
}

After

1
2
3
4
5
6
7
// parses a date in the form of "YYYY-MM-DD hh:mm:ss"
function parseDate(dateStr) {
  var [date, time] = dateStr.split(' ');
  var [year, month, day] = date.split('-').map(Number);
  var [hours, mins, seconds] = time.split(':').map(Number);
  return new Date(year, month - 1, day, hours, mins, seconds);
}

Object Destructuring

Similar to array destructuring, object destructuring is pulls variables out based on their property name and assigns them to an array.

Before

1
2
3
4
5
6
7
8
function getUser(userId, options) {
  var includeAccount = options.includeAccount;
  var getFriends = options.getFriends;
  
  return getUsersFromDatabase(userId, includeAccount, getFriends);
}

getUser(1, { includeAccont: true, getFriends: false });

It is especially useful when working with a module system such as CommonJS (or the ES6 module system) where a set of related functions is bundled into an object but you may only need a few of them.

1
2
3
4
5
6
7
var ui = require('my-ui-framework');
var Input = ui.Input;
var Label = ui.Label;
var Form = ui.Form;
var Panel = ui.Panel; 
var Accordian = ui.Accordian;
// etc...

After

1
2
3
4
5
function getUser(userId, options) {
  var { includeAccount, getFriends } = options;
  
  return getUsersFromDatabase(userId, includeAccount, getFriends);
}
1
var { Input, Label, Form, Panel, Accordian } = require('my-ui-framework');

Enhanced Object Literals

Object literals have been enhanced in ES6 with sugar that makes writing them less verbose.

Property Shorthand

Property shorthand provides a quick way to create objects from a set of variables. The name of the variable will be used as both the key and the value for the property. This is useful in situations where you have a list of variables that you want to combine into a single object, such as in a factory function.

Before

1
2
3
4
5
6
7
function createPerson(firstName, lastName, age) {
  return { 
    firstName: firstName,
    lastName: lastName,
    age: age
  };
}

After

1
2
3
4
5
6
7
function createPerson(firstName, lastName, age) {
  return { 
    firstName,
    lastName,
    age
  };
}

Method Shorthand

Method shorthand is similar to property shorthand in that it simply reduces the number of characters needed to type when defining a method on an object: the “function” part of the method declaration can be dropped.

Before

1
2
3
4
5
6
7
8
function createPerson(firstName) {
  return { 
    firstName: firstName,
    greet: function(greeting) {
      return greeting + ' my name is ' + this.firstName;
    }
  };
}

After

1
2
3
4
5
6
7
8
function createPerson(firstName) {
  return { 
    firstName: firstName,
    greet(greeting) {
      return greeting + ' my name is ' + this.firstName;
    }
  };
}

Computed/Dynamic Properties

Have you ever needed to add a property to an object when creating whose key is based on some other variable or function? In ES5 you would have to create the object and then add the new property to the object. With ES6 computed properties you can add statements directly to the key name so you can create the object without dropping out of the literal notation. Dynamic properties are created by wrapping the statement inside of square braces: []. Perfect when creating objects based on other constants.

Before

1
2
3
4
5
6
7
8
9
10
11
12
var actions = {
   RUN: 'RUN',
   WALK: 'WALK'
};

var commands = {};
commands[actions.RUN] = function() { console.log('running'); };
commands[actions.WALK] = function() { console.log('walking'); };

function executeCommand(action) {
   commands[action]();
}

After

1
2
3
4
5
6
7
8
9
10
11
12
13
var actions = {
   RUN: 'RUN',
   WALK: 'WALK'
};

var commands = {
   [actions.RUN]: function() { console.log('running'); },
   [actions.WALK]: function() { console.log('walking'); }
};

function executeCommand(action) {
   commands[action]();
}

Arrow Functions

Arrow functions (sometimes referred to as “fat arrows” =>) in ES6 are a new way to declare functions that “share” the same this context as the surrounding scope. The basic syntax is that the left hand side of the arrow contains the function arguments and the right hand side contains the statement/expressions. They are similar in syntax to lambda expressions found in many other languages and are ideal for “functional” programming style.

Shorthand

Parentheses on the left hand side of the arrow are optional if there is only one argument, and curly braces on the right hand side are optional if there is a single expression (in this case, the single expression will be returned from the function).

Before

1
2
3
4
5
6
7
8
9
10
// doubles all numbers, excludes all doubled numbers who do not contain the '4' digit in them, and sums the leftover numbers together
function sillyMathProblem(numbers) {
  return numbers.map(function(number) {
    return number * 2;
  }).filter(function(number) {
    return number.toString().indexOf('4') !== -1; 
  }).reduce(function(l, r) {
    return l + r;
  }, 0);
}

After

1
2
3
4
5
6
// doubles all numbers, excludes all doubled numbers who do not contain the '4' digit in them, and sums the leftover numbers together
function sillyMathProblem(numbers) {
  return numbers.map(number => number * 2)
    .filter(number => number.toString().indexOf('4') !== -1)
    .reduce((l, r) => l + r, 0);
}

Lexical ‘this’

Prior to the introduction of arrow functions, all functions defined their own “this” value. This meant that 1) you could never really know for sure what “this” referred to in a function call without seeing how it was actually called and 2) you often had to create temporary variables or use .bind or .call in order to preserve a “this” from an outer scope. With arrow functions, the “this” variable is the same as the outer scope, and can never be changed.

Before

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function UsersService() {
   var users = {};
   
   this.getUser = function(userId) {
     return users[userId];
   }
   
   this.getUsers = function(userIds) {
     var self = this;
     return userIds.map(function(userId) { // or could have used .bind(this) instead of using self
        return self.getUser(userId);
     });
   };
}

After

1
2
3
4
5
6
7
8
9
10
11
function UsersService() {
   var users = {};
   
   this.getUser = function(userId) {
     return users[userId];
   }
   
   this.getUsers = function(userIds) {
     return userIds.map(userId => this.getUser(userId)); // maintains the "this" of the containing scope
   };
}

Conclusion

ES6 provides a whole host of new syntactic sugar that can be used to shorten and clarify your code. However, ES6 is not all about the sweet stuff. In the next few “ES6 and Beyond” posts we’ll discuss features that are not merely shortened versions of current ES5 features. These features include things from the ES6 standard such as classes, Promises, Symbols, iterators, and generators/yield as well as candidate features for JavaScript versions beyond ES6 that have the potential to completely change the way you write your code: decorators, async, and observables.