13. Arrow functions
Table of contents
Please support this book: buy it (PDF, EPUB, MOBI) or donate
(Ad, please don’t block.)

13. Arrow functions



13.1 Overview

There are two benefits to arrow functions.

First, they are less verbose than traditional function expressions:

const arr = [1, 2, 3];
const squares = arr.map(x => x * x);

// Traditional function expression:
const squares = arr.map(function (x) { return x * x });

Second, their this is picked up from surroundings (lexical). Therefore, you don’t need bind() or that = this, anymore.

function UiComponent() {
    const button = document.getElementById('myButton');
    button.addEventListener('click', () => {
        console.log('CLICK');
        this.handleClick(); // lexical `this`
    });
}

The following variables are all lexical inside arrow functions:

13.2 Traditional functions are bad non-method functions, due to this

In JavaScript, traditional functions can be used as:

  1. Non-method functions
  2. Methods
  3. Constructors

These roles clash: Due to roles 2 and 3, functions always have their own this. But that prevents you from accessing the this of, e.g., a surrounding method from inside a callback (role 1).

You can see that in the following ES5 code:

function Prefixer(prefix) {
    this.prefix = prefix;
}
Prefixer.prototype.prefixArray = function (arr) { // (A)
    'use strict';
    return arr.map(function (x) { // (B)
        // Doesn’t work:
        return this.prefix + x; // (C)
    });
};

In line C, we’d like to access this.prefix, but can’t, because the this of the function from line B shadows the this of the method from line A. In strict mode, this is undefined in non-method functions, which is why we get an error if we use Prefixer:

> var pre = new Prefixer('Hi ');
> pre.prefixArray(['Joe', 'Alex'])
TypeError: Cannot read property 'prefix' of undefined

There are three ways to work around this problem in ECMAScript 5.

13.2.1 Solution 1: that = this

You can assign this to a variable that isn’t shadowed. That’s what’s done in line A, below:

function Prefixer(prefix) {
    this.prefix = prefix;
}
Prefixer.prototype.prefixArray = function (arr) {
    var that = this; // (A)
    return arr.map(function (x) {
        return that.prefix + x;
    });
};

Now Prefixer works as expected:

> var pre = new Prefixer('Hi ');
> pre.prefixArray(['Joe', 'Alex'])
[ 'Hi Joe', 'Hi Alex' ]

13.2.2 Solution 2: specifying a value for this

A few Array methods have an extra parameter for specifying the value that this should have when invoking the callback. That’s the last parameter in line A, below.

function Prefixer(prefix) {
    this.prefix = prefix;
}
Prefixer.prototype.prefixArray = function (arr) {
    return arr.map(function (x) {
        return this.prefix + x;
    }, this); // (A)
};

13.2.3 Solution 3: bind(this)

You can use the method bind() to convert a function whose this is determined by how it is called (via call(), a function call, a method call, etc.) to a function whose this is always the same fixed value. That’s what we are doing in line A, below.

function Prefixer(prefix) {
    this.prefix = prefix;
}
Prefixer.prototype.prefixArray = function (arr) {
    return arr.map(function (x) {
        return this.prefix + x;
    }.bind(this)); // (A)
};

13.2.4 ECMAScript 6 solution: arrow functions

Arrow functions work much like solution 3. However, it’s best to think of them as a new kind of functions that don’t lexically shadow this. That is, they are different from normal functions (you could even say that they do less). They are not normal functions plus binding.

With an arrow function, the code looks as follows.

function Prefixer(prefix) {
    this.prefix = prefix;
}
Prefixer.prototype.prefixArray = function (arr) {
    return arr.map((x) => {
        return this.prefix + x;
    });
};

To fully ES6-ify the code, you’d use a class and a more compact variant of arrow functions:

class Prefixer {
    constructor(prefix) {
        this.prefix = prefix;
    }
    prefixArray(arr) {
        return arr.map(x => this.prefix + x); // (A)
    }
}

In line A we save a few characters by tweaking two parts of the arrow function:

13.3 Arrow function syntax

The “fat” arrow => (as opposed to the thin arrow ->) was chosen to be compatible with CoffeeScript, whose fat arrow functions are very similar.

Specifying parameters:

    () => { ... } // no parameter
     x => { ... } // one parameter, an identifier
(x, y) => { ... } // several parameters

Specifying a body:

x => { return x * x }  // block
x => x * x  // expression, equivalent to previous line

The statement block behaves like a normal function body. For example, you need return to give back a value. With an expression body, the expression is always implicitly returned.

Note how much an arrow function with an expression body can reduce verbosity. Compare:

const squares = [1, 2, 3].map(function (x) { return x * x });
const squares = [1, 2, 3].map(x => x * x);

13.3.1 Omitting parentheses around single parameters

Omitting the parentheses around the parameters is only possible if they consist of a single identifier:

> [1,2,3].map(x => 2 * x)
[ 2, 4, 6 ]

As soon as there is anything else, you have to type the parentheses, even if there is only a single parameter. For example, you need parens if you destructure a single parameter:

> [[1,2], [3,4]].map(([a,b]) => a + b)
[ 3, 7 ]

And you need parens if a single parameter has a default value (undefined triggers the default value!):

> [1, undefined, 3].map((x='yes') => x)
[ 1, 'yes', 3 ]

13.4 Lexical variables

13.4.1 Propagating variable values: static versus dynamic

The following are two ways in which the values of variables can be propagated.

First, statically (lexically): Where a variable is accessible is determined by the structure of the program. Variables declared in a scope are accessible in all scopes nested inside it (unless shadowed). For example:

const x = 123;

function foo(y) {
    return x; // value received statically
}

Second, dynamically: Variable values can be propagated via function calls. For example:

function bar(arg) {
    return arg; // value received dynamically
}

13.4.2 Variables that are lexical in arrow functions

The source of this is an important distinguishing aspect of arrow functions:

The complete list of variables whose values are determined lexically is:

13.5 Syntax pitfalls

There are a few syntax-related details that can sometimes trip you up.

13.5.1 Arrow functions bind very loosely

If you view => as an operator, you could say that it has a low precedence, that it binds loosely. That means that if it is in conflict with other operators, they usually win.

The reason for that is to allow an expression body to “stick together”:

const f = x => (x % 2) === 0 ? x : 0;

In other words, we want => to lose the fight against === and ?. We want it to be interpreted as follows

const f = x => ((x % 2) === 0 ? x : 0);

If => won against both, it would look like this:

const f = (x => (x % 2)) === 0 ? x : 0;

If => lost against ===, but won against ?, it would look like this:

const f = (x => ((x % 2) === 0)) ? x : 0;

As a consequence, you often have to wrap arrow functions in parentheses if they compete with other operators. For example:

console.log(typeof () => {}); // SyntaxError
console.log(typeof (() => {})); // OK

On the flip side, you can use typeof as an expression body without putting it in parens:

const f = x => typeof x;

13.5.2 No line break after arrow function parameters

ES6 forbids a line break between the parameter definitions and the arrow of an arrow function:

const func1 = (x, y) // SyntaxError
=> {
    return x + y;
};
const func2 = (x, y) => // OK
{
    return x + y;
};
const func3 = (x, y) => { // OK
    return x + y;
};

const func4 = (x, y) // SyntaxError
=> x + y;
const func5 = (x, y) => // OK
x + y;

Line breaks inside parameter definitions are OK:

const func6 = ( // OK
    x,
    y
) => {
    return x + y;
};

The rationale for this restriction is that it keeps the options open w.r.t. “headless” arrow functions in the future (you’d be able to omit the parentheses when defining an arrow function with zero parameters).

13.5.3 You can’t use statements as expression bodies

13.5.3.1 Expressions versus statements

Quick review (consult “Speaking JavaScript” for more information):

Expressions produce (are evaluated to) values. Examples:

3 + 4
foo(7)
'abc'.length

Statements do things. Examples:

while (true) { ··· }
return 123;

Most expressions1 can be used as statements, simply by mentioning them in statement positions:

function bar() {
    3 + 4;
    foo(7);
    'abc'.length;
}
13.5.3.2 The bodies of arrow functions

If an expression is the body of an arrow function, you don’t need braces:

asyncFunc.then(x => console.log(x));

However, statements have to be put in braces:

asyncFunc.catch(x => { throw x });

13.5.4 Returning object literals

Some parts of JavaScript’s syntax are ambiguous. Take, for example, the following code.

{
    bar: 123
}

It could be:

Given that the body of an arrow function can be either an expression or a statement, you have to put an object literal in parentheses if you want it to be an expression body:

> const f1 = x => ({ bar: 123 });
> f1()
{ bar: 123 }

For comparison, this is an arrow function whose body is a block:

> const f2 = x => { bar: 123 };
> f2()
undefined

13.6 Immediately-invoked arrow functions

Remember Immediately Invoked Function Expressions (IIFEs)? They look as follows and are used to simulate block-scoping and value-returning blocks in ECMAScript 5:

(function () { // open IIFE
    // inside IIFE
})(); // close IIFE

You can save a few characters if you use an Immediately Invoked Arrow Function (IIAF):

(() => {
    return 123
})();

13.6.1 Semicolons

Similarly to IIFEs, you should terminate IIAFs with semicolons (or use an equivalent measure), to avoid two consecutive IIAFs being interpreted as a function call (the first one as the function, the second one as the parameter).

13.6.2 Parenthesizing arrow function with block bodies

Even if the IIAF has a block body, you must wrap it in parentheses, because it can’t be (directly) function-called. The reason for this syntactic constraint is consistency with arrow functions whose bodies are expressions (as explained next).

As a consequence, the parentheses must be around the arrow function. In contrast, you have a choice with IIFEs – you can either put the parentheses around the whole expression:

(function () {
    ···
}());

Or just around the function expression:

(function () {
    ···
})();

Given how arrow functions work, the latter way of parenthesizing should be preferred from now on.

13.6.3 Parenthesizing arrow function with expression bodies

If you want to understand why you can’t invoke an arrow function by putting parentheses immediately after it, you have to examine how expression bodies work: parentheses after an expression body should be part of the expression, not an invocation of the whole arrow function. This has to do with arrow functions binding loosely, as explained in a previous section.

Let’s look at an example:

const value = () => foo();

This should be interpreted as:

const value = () => (foo());

And not as:

const value = (() => foo)();

Further reading: A section in the chapter on callable entities has more information on using IIFEs and IIAFs in ES6. Spoiler: you rarely need them, as ES6 often provides better alternatives.

13.7 Arrow functions versus bind()

ES6 arrow functions are often a compelling alternative to Function.prototype.bind().

13.7.1 Extracting methods

If an extracted method is to work as a callback, you must specify a fixed this, otherwise it will be invoked as a function (and this will be undefined or the global object). For example:

obj.on('anEvent', this.handleEvent.bind(this));

An alternative is to use an arrow function:

obj.on('anEvent', event => this.handleEvent(event));

13.7.2 this via parameters

The following code demonstrates a neat trick: For some methods, you don’t need bind() for a callback, because they let you specify the value of this, via an additional parameter. filter() is one such method:

const as = new Set([1, 2, 3]);
const bs = new Set([3, 2, 4]);
const intersection = [...as].filter(bs.has, bs);
    // [2, 3]

However, this code is easier to understand if you use an arrow function:

const as = new Set([1, 2, 3]);
const bs = new Set([3, 2, 4]);
const intersection = [...as].filter(a => bs.has(a));
    // [2, 3]

13.7.3 Partial evaluation

bind() enables you to do partial evaluation, you can create new functions by filling in parameters of an existing function:

function add(x, y) {
    return x + y;
}
const plus1 = add.bind(undefined, 1);

Again, I find an arrow function easier to understand:

const plus1 = y => add(1, y);

13.8 Arrow functions versus normal functions

An arrow function is different from a normal function in only two ways:

Apart from that, there are no observable differences between an arrow function and a normal function. For example, typeof and instanceof produce the same results:

> typeof (() => {})
'function'
> () => {} instanceof Function
true

> typeof function () {}
'function'
> function () {} instanceof Function
true

Consult the chapter on callable entities for more information on when to use arrow functions and when to use traditional functions.

13.9 FAQ: arrow functions

13.9.1 Why are there “fat” arrow functions (=>) in ES6, but no “thin” arrow functions (->)?

ECMAScript 6 has syntax for functions with a lexical this, so-called arrow functions. However, it does not have arrow syntax for functions with dynamic this. That omission was deliberate; method definitions cover most of the use cases for thin arrows. If you really need dynamic this, you can still use a traditional function expression.

Next: 14. New OOP features besides classes