Real Life ES6 - Arrow Functions
Some of the features soon to be at our fingertips with the growing support for ECMAScript 6 are absolutely fantastic, but often examples shown online are contrived. In this series of blog posts, we'll pick out a few ES6 features and show you some real code that's improved with new features of the language.
This post was written in collaboration with Adam Yeats.
Support
ES6 support is mixed across platforms, so you shouldn't expect to start using this stuff today. Implementations are being added all the time, and I recommend using The ES6 Compatability Table to see the current state of affairs.
Traceur
All the code examples seen in this post were run through Traceur, a tool for compiling ES6 code into ES5 code which has a much better browser support at this time. It allows you to write ES6, compile it and use the result in environments where ES6 features are not implemented. Traceur is installed through npm:
npm install --global traceur
And then used on a source file like so:
traceur --out build.js --script my_source_file.js
You'll also need to include the Traceur runtime in your HTML. The runtime comes as part of the Node module, and is found in the bin/runtime.js
directory.
Arrow Functions
Today we'll focus exclusively on Arrow functions. One of the quickest of quick wins, arrow functions allow us to write less and achieve more. Let's take a look at an example of mapping over an array and performing the same task on each element. The code below maps over an array of objects and turns them into an array containing just one particular property from each object:
var users = [
{ name: 'Jack', age: 21 },
{ name: 'Ben', age: 23 },
{ name: 'Adam', age: 22 },
];
console.log(
users.map(function(user) {
return user.age;
})
);
// [21, 23, 22]
That's really nice, but also feels a little verbose having to type all that. With the new arrow functions, we can write it like so:
var users = [
{ name: 'Jack', age: 21 },
{ name: 'Ben', age: 23 },
{ name: 'Adam', age: 22 },
];
console.log(users.map(user => user.age));
// [21, 23, 22]
Notice how much nicer that feels to read, as well as to type? It's much less code to achieve the same thing. We could then go about summing those ages:
var users = [
{ name: 'Jack', age: 21 },
{ name: 'Ben', age: 23 },
{ name: 'Adam', age: 22 },
];
var ages = users.map(user => user.age);
var sum = ages.reduce((a, b) => a + b);
console.log(sum);
// 66
Because reduce
takes two parameters, brackets are required to make it clear that the parameters are for the arrow function, not for the reduce
call.
Arrow functions can have multiple statements within, in which case you need to use a block. You also need to use the return
keyword, whereas in the one line examples above, the return was implicit.
var users = [
{ name: 'Jack', age: 21 },
{ name: 'Ben', age: 23 },
{ name: 'Adam', age: 22 },
];
var agesDoubled = users.map(user => {
var age = user.age;
return age * 2;
});
However, once you get to this stage it's a good sign that you probably want to be using regular functions - the benefit of the arrow function is definitely for small, one line methods.
Another handy feature of arrow functions is the lexical binding of this
to a function. As you'll probably know already, when you create a new function, the this
keyword is set to a value depending on the way a function is called, and the rules as to what this
might be defined as are notoriously convoluted. Let's see how arrow functions might help us out here, using a trivial example of creating an API wrapper that returns a Promise (another great ES6 feature that we'll cover very soon). Consider the following example:
function API() {
this.uri = 'http://www.my-hipster-api.io/';
}
// let's pretend this method gets all documents at
// a specific RESTful resource...
API.prototype.get = function(resource) {
return new Promise(function(resolve, reject) {
// this doesn't work
http.get(this.uri + resource, function(data) {
resolve(data);
});
});
};
var api = new API();
// by calling this method, we should be making a request to
// http://www.my-hipster-api.io/nuggets
api.get('nuggets').then(function(data) {
console.log(data);
});
So what's wrong here? Well, aside from not being the best example of Promise usage in the world (it's generally considered a bit of an anti-pattern to wrap a callback function in this way), this.uri
is undefined
so when we come to call our http.get()
method that we're wrapping, we can't properly form the URL we need. Why would this be? Well, when we call new Promise()
, we're calling a constructor of another object, which creates a new lexical this
in turn. Put simply, this.uri
is not in scope.
Today, we can work around this in a few ways. We could have written something like this:
API.prototype.get = function(resource) {
var self = this; // a-ha! we'll assign to a local var
return new Promise(function(resolve, reject) {
// this works!
http.get(self.uri + resource, function(data) {
resolve(data);
});
});
};
...and, lo and behold, it works! By creating a variable that points to this
, we can access it from any of our inner functions. In fact, if we were to use Traceur to transpile our ES6 into ES5 compatible code, it actually outputs something very similar to the above pattern. But we shouldn't have to do this, right? Surely there must be a way for us to define this
ourselves? If we're working inside an environment where we have ES5 features (IE9 or above), we could use .bind()
, which is a method on the Function
prototype that allows us to "bind" (funnily enough) a value a function's lexical this
.
API.prototype.get = function(resource) {
return new Promise(
function(resolve, reject) {
// this works!
http.get(this.uri + resource, function(data) {
resolve(data);
});
}.bind(this)
);
};
This works, but could be a little tidier. If we decide to nest a few callbacks within each other, and they all need access to the outer function's this
keyword, then we have to affix .bind()
to every nested function. There are also performance implications in using .bind()
, but likely (hopefully) these will be fixed in due time.
Enter arrow functions! In ES6, the same function above could be defined like this:
API.prototype.get = function(resource) {
return new Promise((resolve, reject) => {
http.get(this.uri + resource, function(data) {
resolve(data);
});
});
};
It certainly looks a bit more concise, but what's the arrow doing? Well, it actually binds the context of the Promise's this
to the context of the function that contains it, so this.uri
resolves to the value we assigned in the constructor. This avoids having to use bind
or the dreaded var self = this
trick to keep a reference to the desired scope.