ES2015 (ES6) introduces a really nice feature that punches above its weight in terms of simplicity to integrate versus time saving and feature output. This feature is the arrow function.
Before we dive into the features of the arrow function and what it actually does for us, let’s understand what an arrow function is not. It’s not a replacement for the function
keyword, at all. This means you can’t do a find and replace on every single function
keyword and everything works perfectly, because it likely won’t.
If you’re competent with the way JavaScript scope works, and have a great understanding of lexical scope, the this
keyword and Prototype methods such as .call()
, .apply()
and .bind()
, then you’re in good hands to continue reading.
Syntax
Let’s look at what the arrow function’s construct is from MDN:
// example 1
([param] [, param]) => {
statements
}
// example 2
param => expression
The “normal JavaScript” (ES5) equivalents to help transition:
// example 1
function ([param] [, param]) {
statements
}
// example 2
function (param) {
return expression
}
The ES6 and ES5 differences in example 1
are that the function
keyword is omitted, and =>
now exists after the arguments. In example 2
, our function has been reduced to one line, this is great for single line function expressions that get return
‘d.
Hint: arrows are anonymous
Arrow functions are always anonymous, which means we can’t do this with ES6:
// ES5
function doSomething() {
//...
}
Instead of this, we could assign our anonymous arrow function it to a variable (using var
here instead of let
as ES6 block scoping is another topic):
// ES6
var doSomething = () => {
//...
}
Let’s look at the syntaxes a little further and then the functionality differences when using arrow functions.
Syntax: single line expressions
We touched briefly above on single line expressions, let’s look at a great use case for them.
Let’s take some junky ES5 example that iterates over an Array using Array.prototype.map
:
var numbers = [1,2,3,4,5];
var timesTwo = numbers.map(function (number) {
return number * 2;
});
console.log(timesTwo); // [2, 4, 6, 8, 10]
We can reduce this down to a single line with an arrow function, which saves us a lot of typing and can actually enhance readability in my opinion as this piece of code has one clear role:
var numbers = [1,2,3,4,5];
var timesTwo = numbers.map((number) => number * 2);
console.log(timesTwo); // [2, 4, 6, 8, 10]
Syntax: single argument functions
Arrow functions also give us a small “sugar” syntax that allows us to remove parenthesis when only using a single argument in a function.
Taking the last piece of code for example we had this:
numbers.map((number) => number * 2);
When we could remove the parens from (number)
to leave us with this:
numbers.map(number => number * 2);
This is great and a little clearer initially, but as we all know applications grow and code scales, and to save us headaches (be it forgetting syntaxes or lesser experienced developers “not knowing” to add parens back with more than one argument), I’d recommend always using the parens out of habit, even for single args:
// we still rock with ES6
numbers.map((number) => number * 2);
Functionality: lexical scoping “this”
Now we’re past the sugar syntax excitement, we can dig into the benefits of the arrow function and its implications on execution context.
Typically if we’re writing ES5, we’ll use something like Function.prototype.bind
to grab the this
value from another scope to change a function’s execution context. This will mainly be used in callbacks inside a different scope.
In Angular, I adopt the controllerAs
syntax which allows me to use this
inside the Controller to refer to itself (so here’s an example). Inside a function the this
value may change, so I could have a few options, use that = this
or .bind
:
function FooCtrl (FooService) {
this.foo = 'Hello';
FooService
.doSomething(function (response) {
this.foo = response;
});
}
The this.foo = response;
won’t work correctly as it’s been executed in a different context. To change this we could use .bind(this)
to give our desired effect:
function FooCtrl (FooService) {
this.foo = 'Hello';
FooService
.doSomething(function (response) {
this.foo = response;
}.bind(this));
}
Or you may be used to keeping a top level this
reference, which can make more sense when dealing with many nested contexts, we don’t want a gross tree of .bind(this), .bind(this), .bind(this)
and a tonne of wasted time binding those new functions (.bind
is very slow). So we could look at that = this
to save the day:
function FooCtrl (FooService) {
var that = this;
that.foo = 'Hello';
FooService
.doSomething(function (response) {
that.foo = response;
});
}
With arrow functions, we have a better option, which allows us to “inherit” the scope we’re in if needed. Which means if we changed our initial example to the following, the this
value would be bound correctly:
function FooCtrl (FooService) {
this.foo = 'Hello';
FooService
.doSomething((response) => { // woo, pretty
this.foo = response;
});
}
We could then refactor some more into a nice single line expression, push to git and head home for the day:
function FooCtrl (FooService) {
this.foo = 'Hello';
FooService
.doSomething((response) => this.foo = response);
}
The interesting thing to note is that the this
value (internally) is not actually bound to the arrow function. Normal functions in JavaScript bind their own this
value, however the this
value used in arrow functions is actually fetched lexically from the scope it sits inside. It has no this
, so when you use this
you’re talking to the outer scope.