ES6: What are the benefits of the new features in practice?

Tags:

You’ve probably seen people talk about the features in JavaScript’s 6th edition, ES6, or ECMAScript 6. Promises, let keyword, iterators, etc.

But why should you care? I certainly heard a lot about it, but I hadn’t looked into it because I wasn’t interested in theory, but rather the benefits I would get in my day to day work. Is it just feature-bloat for sake of having fancy things in the language, or are they actually useful?

I then did some research and discovered a number of very useful features in ES6 that you can start taking advantage of right now. Let’s take a look at the most useful ones

Can you actually use ES6?

Before we go ahead, let’s address a critical issue: Can you use these features without worrying that it’ll break in older browsers?

Edge, Chrome, Firefox and Safari all have most of ES6 features implemented. With older browsers, you will either need a shim for some parts of ES6, or you need to convert ES6 into ES5 (or lower) using a tool such as Babel.

If you want to learn how to get set up with ES6, check out my post on how to get started with ES6. It has all the information you need to get going, so in this article I’ll focus on the features and their benefits.

let and const

The let keyword defines a block-scoped variable. Block-scoped means the declaration is only available within a so-called “block”.

Compare how it behaves vs the var keyword:

function foo() {
  //whenever we have curly braces in code, it defines a "block"
  {
    console.log(hi); //error, the variable is not defined
 
    //this is only available within this block
    let hi = 1;
  }
  console.log(hi); //output: Error, hi is not defined
 
  for(let i = 0; i < 10; i++) {
    //the variable `i` is only available within the for block
  }
 
  console.log(i); //output: Error, i is not defined
}

Swap in some vars instead of lets…

function foo() {
 
  {
    //with var, the variable declaration gets hoisted so we will not get an error
    //when attempting to use it before its declaration appears in code
    console.log(hi); //output: undefined
 
    var hi = 1;
  }
  console.log(hi); //output: 1
 
  for(var i = 0; i < 10; i++) {
    //the variable `i` is only available within the for block
  }
 
  console.log(i); //output: 10
}

Variables declared using var are also hoisted, as shown with the log statement, which can cause unexpected behavior to those unfamiliar with JavaScript’s hoisting rules.

const declares a block-scoped constant. You may be familiar with constants from other languages, and here they are the same: A constant is like a variable, except you can’t change its value.

const foo = 1;
foo = 2; //error: cannot change the value of a constant

There is one small gotcha when using const. When assigning an object, you will still be able to modify its properties!

const obj = { hello: 'world' };
obj.hello = 'galaxy'; //no error!

But assigning another object, even if it has the same properties, will cause an error:

const obj = { hello: 'world' };
obj = { hello: 'galaxy' }; //error

A workaround exists for this, using Object.freeze:

const obj = { hello: 'world' };
Object.freeze(obj);
obj.hello = 'galaxy'; //this will now cause an error

(thanks Reddit user jcready for this tip)

Benefits of let

A major benefit of let is it works nicely within for-loops. One of the most common stumbling blocks for JS-beginners is the closure-within-for problem:

for(var i = 0; i < 10; i++) {
  foo[i].onclick = function() {
    console.log(i);
  };
}

Seasoned JS developers recognize this and know that the click handlers will all output 10.

If we rewrite this using let

for(let i = 0; i < 10; i++) {
  foo[i].onclick = function() {
    console.log(i);
  };
}

…it now behaves as could be expected: the handlers will output the value of i from their respective iterations.

To me, the behavior with let is better than var. let does everything, making var feel unnecessary. For an opposite point of view, check out Kyle Simpson’s in-depth article, for and against let.

Benefits of const

Constants are perhaps a bit less common, but const can bring many benefits when making sure your code is correct and easy to reason about.

Sometimes you need to declare values in functions which should not be modified – in other words, constants – and using const there gives two benefits:

  1. The person reading the code immediately sees the variable should not be changed
  2. Changing the value will result in an error

Using const to indicate values that shouldn’t change, you also reduce the likelihood of bugs. The code cannot redeclare or change the value, guaranteeing that the value keeps its meaning. Bugs have been known to occur because another programmer accidentally changed a variable like that – using const eliminates the possibility.

For example, one case is functions calling other functions to get data, or initializing helper variables. These can be declared const, as they are often not changed.

Using const together with let and var, it’s easier to communicate the intent of your code, which helps to make the code self-documenting.

Computed property names

Have you ever had to write something like this?

var obj = { };
obj[prop] = 'some value';

I certainly have. In ES5 there is no way to use a variable directly in an object literal, so you need to assign it separately like above.

It’s only a minor issue, but ES6 fixes it by allowing the following syntax:

var obj = { [prop]: 'some value' };

Another trick is more dynamic property names:

//this will assign to obj.foo10
var obj = { ['foo' + 10]: 'some value' };

You can put pretty much any code inside the square brackets.

Not a life changing feature, but convenient in some cases.

Default parameters

A common source of confusion to those unfamiliar with JavaScript syntax tricks is the following pattern:

function hello(name) {
  name = name || 'Anonymous';
 
  console.log('Hi ' + name);
}

This syntax is often used to give function parameters default values. Even when you do know this syntax, it fails spectacularly when dealing with booleans, numbers or other falsy values. In other words, it would be better to avoid this pattern, but unfortunately there hasn’t been a convenient way to have default values… until now.

ES6 allows us to define default values for function parameters!

function hello(name = 'Anonymous') {
  console.log('Hi ' + name);
}

The benefits should be reasonably clear:

  • Less boilerplate code to handle parameters
  • Anyone reading your code immediately sees which parameters are optional, without having reading the function body (where they might or might not be on top of the function).
  • This can be useful when refactoring, as you can change a function to have default parameters instead of removing a parameter altogether to keep better backwards-compatibility

One potential gotcha of this syntax is that variables without default parameters still receive undefined as their default, rather than throwing an error if not called with enough parameters. This is how JavaScript has always worked like, but especially for newcomers this could be a point of confusion.

Passing undefined as the value for something with a default parameter defined will also use the default parameter instead of undefined. It’s better to use null to represent cases of “no value” instead of undefined, it will avoid this issue.

There’s also an interesting trick that you can use with defaults to enforce required parameters. You can read about it here.

Destructuring assignment

Destructuring assignment, available in many other languages, can be a very convenient feature when dealing with objects or arrays.

Let’s say we want to do this:

var name = user.name;
var email = user.email;

With ES6 we can use what’s called a destructuring assignment:

var { name, email } = user;

Alternatively, if we want to use variable names which differ from the property names…

//this declares two variables, userName and userEmail
var { name: userName, email: userEmail } = user;

Which is functionally equivalent to:

var userName = user.name;
var userEmail = user.email;

Where is this useful?

A common pattern is using objects to pass around large numbers of parameters, especially if the parameters are optional. That scenario is where destructuring shines:

function complex({ name, email, phone = null }) {
  //variables `name`, `email`, and `phone` are available here
}
 
var user = {
  name: 'foo',
  email: 'bar',
  phone: '123456'
};
complex(user);

Note we used destructuring in the function parameter list – it also works there, and even supports defaults!

By doing this, we get a number of benefits:

  • We make the properties the object parameter requires explicit. A common issue is seeing a function takes an object, but not knowing what kind of object. Now that’s solved.
  • We can have defaults. No need to check if the property is there
  • We make optional properties explicit. By providing a default for a property, anyone reading the code sees the property is optional.

Explicitly visible things make code easier to maintain, as you don’t have to guess or read the whole function to figure it out.

You can do many fancy things with destructuring. For comprehensive examples, you should check Scott Allen’s post on ES6 destructuring.

Be careful to not take things too far. It’s possible to make your code harder to read with too much use of destructuring:

let { address: { home: { street } } = user;
 
//is equivalent to:
let street = user.address.home.street;

You can probably see the example without destructuring above is much easier to understand.

Object initializer shorthand

This feature allows you to skip repeating yourself in object literals.

A pattern seen in the nodejs world:

module.exports = {
  someFunction: someFunction,
  otherFunction: otherFunction
};

Sometimes you also see the same with the module pattern:

var myModule = (function() {
  /* declarations here */
 
  return {
    someFunction: someFunction,
    otherFunction: otherFunction
  };
})();

Using ES6’s object initializer shorthand, we can rewrite it:

module.exports = {
  someFunction,
  otherFunction
};
var myModule = (function() {
  /* declarations here */
 
  return {
    someFunction,
    otherFunction
  };
})();

Again a feature without huge benefits, but reducing duplication is always a good thing.

Rest and spread parameters

Rest and spread parameters introduce new syntax for functions which take an arbitrary number of parameters, or when using arrays as parameters.

In ES5, to write a function taking multiple parameters (such as Array#push)..

function push() {
  var args = Array.prototype.slice.call(arguments);
  var list = args.shift();
 
  args.forEach(function(a) {
    list.push(a);
  });
}

In other words, we need to use the arguments object, which is not ideal. We also can’t describe the function’s ability to take any number of args very easily without using comments. This leads to confusing code.

Using rest parameters in ES6, we can instead write the following:

function push(list, ...values) {
  values.forEach(function(v) {
    list.push(v);
  });
}

This code automatically creates an array of all the parameters after the first, and the array is assigned to the variable values.

This is much more readable at a glance: You can immediately tell this function can take any number of args. And even better, we don’t need the boilerplate for using arguments.

Spread parameters is similar:

var values = [1,2,3];
var target = [];
target.push(...values);

The syntax with ...list above is same as this:

target.push(1, 2, 3);

Although a bit situational, this can be a useful feature in some cases.

Template literals

At simplest, template literals allow easy interpolation of variables into strings, and easier multiline strings.

var name = "Björn";
 
var text = `Hello ${name}`;
console.log(text);

The output would be “Hello Björn” as you might guess.

Multi-line strings work as follows:

var text = `Foo
bar
baz`;

The more complex side of template strings is called “tags”:

var sample = foo`Some text here`;

In the above example, foo is a so-called “tag”, which are functions that can transform the string.

For example, we can define a naive text-reverse tag..

function reverse(strings, ...values) {
  var result = '';
  for(var i = 0; i < strings.length; i++) {
    result += strings[i];
    if(i < values.length) {
      result += values[i];
    }
  }
 
  return result.split('').reverse().join('');
}
 
var foo = 'bar';
console.log( reverse`Hello World ${foo}` );

The output is “rab dlroW olleH”.

A text-reversing tag is not very useful, could it be used for something else?

As an example, we could have a tag to auto-escape SQL strings:

db.query(sql`SELECT * FROM x WHERE foo = ${parameter}`);

This is only scratching the surface, but template strings are a powerful feature and I hope library authors will make use of it in the future.

Promises

Promises are a way of dealing with asynchronous code. Code using promises can make asynchronous code easier to compose, and can also avoid the common issue of “callback hell”.

Let’s look at a simple example to show why promises can be useful vs. callbacks. Say you have three asynchronous functions and you need to run all three, and then execute another function:

//for sake of example, we're just using setTimeout. This could be any asynchronous action as well.
function asyncA(callback) { setTimeout(callback, 100); }
function asyncB(callback) { setTimeout(callback, 200); }
function asyncC(callback) { setTimeout(callback, 300); }
 
asyncA(function(resultA) {
  asyncB(function(resultB) {
    asyncC(function(resultC) {
      someFunction(resultA, resultB, resultC);
    });
  });
});

Compare to an implementation using promises:

function promiseA() { return new Promise((resolve, reject) => setTimeout(resolve, 100)); }
function promiseB() { return new Promise((resolve, reject) => setTimeout(resolve, 200)); }
function promiseC() { return new Promise((resolve, reject) => setTimeout(resolve, 300)); }
 
Promise.all([promiseA(), promiseB(), promiseC()]).then(([a, b, c]) => {
  someFunction(a, b, c);
});

Promises allow easily composable asynchronous functions, without creating a callback-pyramids like in the first example. We do need a bit more code to create the promise in the first place, but if our asynchronous functions were longer, it wouldn’t be such a visible difference.

This is only one of the advantages of using promises, and it’s a very broad topic. If you are unfamiliar with promises and their benefits, I recommend reading Kyle Simpson’s take on promises in his book You Don’t Know JS (free to read online).

Promises can simplify asynchronous code a lot, but it can be hard to use them to their full potential. Check out my list of the best resources to learn about Promises for more.

Arrow functions

Arrows is a new syntax for functions, which brings several benefits:

  • Arrow syntax automatically binds this to the surrounding code’s context
  • The syntax allows an implicit return when there is no body block, resulting in shorter and simpler code in some cases
  • Last but not least, => is shorter and simpler than function, although stylistic issues are often subjective

The syntax is quite flexible:

//arrow function with no parameters
var a1 = () => 1;
 
//arrow with one parameter can be defined without parentheses
var a2 = x => 1;
var a3 = (x) => 1;
 
//arrow with multiple params requires parentheses
var a4 = (x, y) => 1;
 
//arrow with body has no implicit return
var a5 = x => { return 1; };

this binding

How often do you see something like this done in JS?

var self = this;
el.onclick = function() {
  self.doSomething();
};

This pattern crops up occasionally, especially in less experienced dev’s code. The solution is to use bind to fix this in the function, but it’s a bit verbose.

With arrows, we can instead just do this:

el.onclick = () => this.doSomething()

Implicit return

The arrow syntax allows two different styles…

With a statement body:

el.onclick = (x, y, z) => {
  foo();
  bar();
  return 'baz';
}
 
//equivalent to:
el.onclick = function(x, y, z) {
  foo();
  bar();
  return 'baz';
}.bind(this);

A statement body allows multiple statements as shown above.

An arrow function with an expression body looks like the example we saw earlier:

el.onclick = (x, y, z) => this.doSomething()
 
//equivalent to
el.onclick = function(x, y, z) {
  return this.doSomething();
}.bind(this);

An expression body allows only a single expression, and has an implicit return. This means it’s great for functional-style code, for example when using Array#map or Array#filter

//ES5 style
var names = users.map(function(u) { return u.name; });
 
//ES6 style
var names = users.map(u => u.name);

ES6 modules

The CommonJS module specification has become increasingly common. First, Node.js adopted it, starting the downfall of the competing AMD and require.js module specifications. Recently tools like Browserify and Webpack have made CommonJS modules the best choice for JavaScript code running in the browser as well.

The native support for ES6 modules is still lacking, but thanks to the tools mentioned above, we can easily use the ES6 modules with our JS code.

Here’s a comparison of how ES6 modules are defined vs CommonJS modules:

export const magicNumber = 42;
 
export function amazingFunction() { return 'the magic number is ' + magicNumber; }
 
//vs CommonJS...
const magicNumber = 42;
 
function amazingFunction() { return 'the magic number is ' + magicNumber; }
 
module.exports = { magicNumber, amazingFunction };

The ES6 module version allows us to write our module with some less repetition – anything we define as being exported is also available within the module itself. In the example with CommonJS, we need to define the value first, and then separately define the export using module.exports.

The syntax for importing modules is also slightly more concise compared to CommonJS:

import { magicNumber } from './my-module';
 
//vs CommonJS...
const { magicNumber } = require('./my-module');

There’s a number of additional options available with ES6 modules. Exploring ES6’s chapter on Modules is a good resource to learn more.

Iterators and Generators

Another new feature is iterators: you can make “iterable” objects, as in similar to how you can iterate through arrays using loops. You do this either with a new syntax called symbols, which allows for special object properties with special functionality, or with another new feature called generators.

Generators can yield values. The function’s execution pauses at the yielded value, and the next time the generator is called, it resumes from where it left off. This can also be used for dealing with asynchronous code in another interesting way, but I’ll talk about that in another article as it’s a big topic on its own.

For working with iterators, ES6 also provides a new looping syntax called for-of:

var arr = [1, 2, 3];
for(var value of arr) {
  console.log(value);
}
//output: 1, 2, 3

This is similar to how foreach-loops work in other languages. Another similarity with foreach is that this also works with iterators, so you can use your own iterable objects with for-of.

Iterators are mainly useful for library authors. It could be useful to offer an iterator-based interface for a library, but in most circumstances you won’t need them.

In closing

I’m sure you’re asking now “What about the new class syntax! ES6 has the new class syntax, right?” – well, that’s correct. However, the class syntax is only syntactic sugar over JavaScript’s prototypal OOP. It doesn’t change anything about how the system works, it merely provides some convenience if you prefer that style.

There aren’t really any benefits from the syntax over just working with plain objects. The only case where the class syntax could be useful is with heavy inheritance, but it’s often better to use composition instead. This is a big topic on its own, and outside the scope of this article.

Overall the new features in ES6 provide a number of benefits especially for larger applications. If you don’t need to support older browsers, then you’ll have a fairly easy time since ES6 features are well supported across recent browsers. For older ones, you may need to use Babel to compile the code into something that works. Kangax’s ES6 compatibility table is a good resource to use if you’re not sure what you need.