ES2015 Arrow Functions

Kerri Shotts
codeburst
Published in
10 min readJun 20, 2017

--

Examples of ES2015 Arrow Functions

ES2015 arrow functions are a useful language feature that can reduce boilerplate which also comes with some very interesting changes with regard to function context (otherwise known as the dreaded this). I’ve taken to using arrow functions quite a bit, but before you do the same, there are some things you should know!

Grammar

So, we hopefully all know what a normal function looks like:

function double(x) {
return x * 2;
}
[1, 2, 3].map(double); // [2, 4, 6]

Arrow functions, on the other hand, look like the following:

let double = x => x * 2;
[1, 2, 3].map(double);

We could even inline the double function if we didn’t need it elsewhere:

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

The syntax here is a bit… terse, to say the least, and there are definitely some sharp edges here where you can hurt yourself. But there’s a lots of benefits as well, not least of which is a little less typing!

It should be obvious whence arrow functions get their name — that => token looks an awful lot like an arrow, right? An arrow function consists of the combination of an arrow token, a list of parameters to the left of the token, and either an expression (returning an implicit result) or a block on the right.

Note: How do you pronounce an arrow function? Give Paul Thaden’s article entitled How do you pronounce “fat arrow”? a read. There’s lots of options available, but no consensus that I’ve heard thus far.

The parameter list is generally wrapped in parentheses, but by convention, a single parameter is not wrapped. This leads to the following forms:

// no parameters
[1, 2, 3].map(() => Math.floor(Math.random() * 3));
// one parameter
[1, 2, 3].map(x => x * 2); // no parentheses is the convention
[1, 2, 3].map((x) => x * 2); // but JS won't complain if you do this
// two or more parameters
[1, 2, 3].map((x, idx) => x * idx); // [0, 2, 6]
// with a block instead of an expression; return IS REQUIRED or the
// resulting array is full of `undefined`.
[40, 20, 10, 5].map(x => {
let str = "";
for (let i = 0; i < x; i++) {
str += "#";
}
return str; // [A]
});

Note: JavaScript won’t complain if you always use parentheses around the parameter list, but I personally drop the parentheses when using only one parameter. If you want to be absolutely consistent, you can configure ESLint’s arrow-parens rule to keep you in line. See http://eslint.org/docs/rules/arrow-parens for more information.

Many Ambiguous Returns…?

It’s at this point that some readers will notice that we’ve now introduced some ambiguity with regard to the return values from arrow functions. For example, how would we return an object literal using an arrow function? We might naively assume we could do this:

[97, 98, 99].map(ascii => {
char: String.fromCharCode(ascii)
});
// [undefined, undefined, undefined]

Wait… what?

It makes more sense once one rewrites it to more closely match what JavaScript’s parser is really seeing, which I’ve done below.

[97, 98, 99].map(function(ascii) {
char:
String.fromCharCode(ascii)
});

Notice that char is not being interpreted as a key but a label instead. The next line does execute, but since it doesn’t return anything, the entire array consists of undefined.

You can encounter even stranger behavior if you’re trying to return multiple properties, like so:

[97, 98, 99].map(ascii => {
ascii: ascii,
char: String.fromCharCode(ascii),
});
// SyntaxError: Unexpected token -- not what you expected, was it?

So, what’s the proper way to return an object literal? Well… with more parentheses as done below…

[97, 98, 99].map(ascii => ({
ascii,
char: String.fromCharCode(ascii)
}));
// [ { ascii: 97, char: "a" }, ... ]

or with an explicit return using block form:

[97, 98, 99].map(ascii => { 
return {
ascii,
char: String.fromCharCode(ascii)
};
});

Note: If you want to enforce consistency, be sure to check out ESLint’s arrow-body-style rule (http://eslint.org/docs/rules/arrow-body-style). The default setting is to warn you when your code is ambiguous, but you can adjust it to your preference.

Nesting Arrow Function Expressions

Arrow functions can also be nested within arrow functions, but if you’re going to do so, you should exercise extreme care, especially when using the short form. JavaScript won’t mind if you throw a terrifying mess of nested arrow functions at it, but you might tomorrow, and your team members will probably complain the moment after you make the commit.

Consider the following code snippet that builds an 8 x 8 grid consisting of row and column references:

const CRLF = [13, 10].reduce((a, c) => a += String.fromCharCode(c), 
"");

const ROWS = [1, 2, 3, 4, 5, 6, 7, 8];
const COLS = ["a", "b", "c", "d", "e", "f", "g", "h"];
const LINE = COLS.map(col => "--").join("-+-");
// [A]
let board = ROWS.map(row => COLS.map(col => col + row).join(" | "))
.join(CRLF + LINE + CRLF);
console.log(board);
// a1 | b1 | c1 | d1 | e1 | f1 | g1 | h1
// ---+----+----+----+----+----+----+---
// a2 | b2 | c2 | d2 | e2 | f2 | g2 | h2
// ...
// a7 | b7 | c7 | d7 | e7 | f7 | g7 | h7
// ---+----+----+----+----+----+----+---
// a8 | b8 | c8 | d8 | e8 | f8 | g8 | h8

The nested arrow functions occur at [A] — an inner arrow function mapping over the columns (col => col + row) and an outer function that maps over the rows. This example isn’t a nightmare to read through, but you can imagine that if you nested much deeper that reading the code would become progressively more and more difficult.

Note: If you’re using the block form and intending properly, reading isn’t as much a problem — no more so than reading nested functions, anyway. That said, you should definitely try to avoid the pyramid of doom, so don’t nest too deeply!

Where Arrow Functions Excel

We’ve seen a lot of examples of arrow functions in conjunction with map but arrow functions are a great fit in a lot of JavaScript constructs, such as the functional methods we’ve already seen, but also callbacks, event handlers, and promise chains.

Staying within the functional paradigm for a moment, arrow functions make sorting or filtering extremely simple, especially when using simple expressions, as in the following example.

let fruits = ["banana", "apple", "orange", "pineapple"];
console.log(fruits.sort((a, b) => a.length - b.length));
console.log(fruits.filter(fruit => fruit.length > 5));

Or, consider a slightly more complex (and admittedly contrived) example that is full of arrow functions (marked with <--).

function SlowDivide(a, b, progress) {
const DELAY = Math.floor(Math.random() * 1000) + 500;
let timerId, time = 0;
if (typeof progress === "function") {
timerId = setInterval(() => // <--
progress((time += 100) / DELAY), 100);
}
return new Promise((resolve, reject) => { // <--
setTimeout(() => { // <--
if (timerId) {
clearInterval(timerId);
}
if (b === 0) {
reject(new Error("Can't divide by zero"));
} else {
resolve (a / b);
}
}, DELAY);
});
}
SlowDivide(10, 0,
p => console.log(`progress: ${Math.floor(p * 100)}%`)) // <--
.then(v => console.log(v)) // <--
.catch(err => console.error(err)); // <--

For me, typing function doesn’t take long, but here I’ve avoided it six times. Over the course of a lot of code, that adds up!

Arrow Function and This

So far what we’ve discussed about arrow functions has been in terms of reducing our typing by a small amount, and perhaps improving readability to a small degree (whether the degree is negative or positive is in the eye of the beholder), but arrow functions also have a huge difference when compared to normal functions: they don’t bind to a new context. In fact, they can’t bind to anything at all — the value of this comes from the surrounding context instead.

Wait… what?

An example might be more useful. Consider the following:

class ClickyButton {
constructor({text, msg} = {}) {
this.text = text;
this.msg = msg;
}
doTheClicky() {
alert(this.msg);
}
render() {
let el = document.createElement("button");
el.textContent = this.text;
el.addEventListener("click", this.doTheClicky, false); // [B]
return el;
}
}

What happens when we render the component and someone clicks the button? An alert will display, but it will contain the text “undefined”, not the desired message, which elicits the following expression from me:

My face every time `this` does not behave as expected

The typical ES5 fix is to change line [B] to bind our event handler to the desired context (namely the current instance of ClickyButton):

el.addEventListener("click", this.doTheClicky.bind(this), false);

However, if we change [B] to use an arrow function instead, and because arrow functions don’t bind to anything, we can use this from the enclosing scope instead, which will alert the proper message:

el.addEventListener("click", () => this.doTheClicky(), false);

A further benefit of this pattern is revealed when you need to pass along parameters — the arrow function is (to my eye) more self-documenting:

el.addEventListener("click", evt => this.doTheClicky(evt), false);

Note: This is often called lexical binding — the value of this is dependent entirely upon where the arrow function is defined.

Arrow Function Gotchas

Due to the lack of a this binding, arrow functions have some very important differences from normal functions and as such, you should remember that arrow functions are not drop-in replacements for regular functions. So while you may be tempted to use them everywhere you would a normal function, if you do so, you will eventually discover a place where doing so causes errors.

Arrow functions:

  • Can’t be used as a constructor
  • Don’t have their own prototype
  • Don’t have their own arguments
  • Can’t yield , eliminating use as generators
  • Shouldn’t be used as object methods ( this will be the wrong value! )
  • this can’t be changed (as there’s no this to change)

Arrow Functions are not Constructors

In order for a function to be a constructor, it must have its own this , which arrow functions lack. Trying to use an arrow function yields a TypeError as evidenced below:

let Animal = kind => this.kind = kind;
let animal = new Animal("dog");
// TypeError: Function is not a constructor

This also means that there’s no associated prototype either:

console.log(Animal.prototype); // undefined

Arrow Functions Inherit the Enclosing Function’s Arguments Object

Normal functions can use arguments with abandon, and so can arrow functions, but arrow functions will be referring to the enclosing function’s arguments , like so:

function foo() {
setTimeout(() => alert([...arguments].join()), 1000);
}
foo("hello", "world"); // alerts "hello,world"

If arrow functions behaved like normal functions, the alert would actually have been empty, having no arguments to collect into an array and join together.

Arrow Functions can’t be used as Generators

For now, you’ll just have to take my word on this one or browse the sources at the end of this article. We’ll cover generators in a future article.

Arrow Functions shouldn’t be used as Object Methods

Although JavaScript doesn’t prevent you from using arrow functions as if they were object methods, you’re not going to have great results if you try. The reason is simply that this will almost certainly not be what you expect.

For example (assuming this code is in the global context):

let dog = {
identifier: "Snoopy",
speak: text => `${this.identifier} says ${text}`
};
console.log(dog.speak("woof!")); // undefined says woof!

I would have expected “Snoopy says woof!”, but that’s not we got. Instead, the current context happens to be the global context, and so this will refer to window, not dog, hence “undefined says woof!”.

Arrow Functions can’t be Bound

We’ve already indicated that arrow functions don’t bind to this, but that has huge implications for any function or library that expects a function that can be bound.

Let’s start with a simple example (again, assuming the following code is in the global context):

function foo(x) {
return this.x * x;
}
const bar = x => this.x * x;foo.call({x: 4}, 2); // 8 this = {x: 4}bar.call({x: 4}, 2); // NaN this = window or global

In the above example, foo isn’t bound to anything, so we’re free to apply a context of our choosing. But bar isn’t bound to anything, and so references to this refer to the enclosing context — in this case, the global context, which will probably not have an x property. Thus this.x is undefined (and undefined multipled by a number is NaN).

This does mean that it’s possible to call a function in some library and see it misbehave because it expects a function that can be bound to something. A common example is when writing tests — you may need to access the test’s context to set a timeout perhaps, but if you’ve used an arrow function, this is definitely not what you were expecting:

describe("a really good test", () => {
it("should work!", done => {
this.timeout(20000); // yeah... no; not going to work!
// undefined is not a function
}
}

Note: Mocha warns explicitly against this — see http://mochajs.org/#arrow-functions.

Performance

As we discovered in the previous article about let and const , not all ES2015 features are necessarily faster than their ES5 counterpart. Arrow functions, however, don’t seem to fall into that bin — as far as I can tell, arrow functions are neither better nor worse than normal functions in most cases.

http://incaseofstairs.com/six-speed/ is an excellent resource that shows that as of the start of 2017 that performance is generally the same, except when referencing arguments in Safari (slow) and that declaring arrow functions can be a bit slower in all browsers (but especially slow in Firefox).

As with all benchmarks, however, take them with a grain of salt. I would generally not avoid arrow functions based solely on these benchmarks, especially since engine performance will be continuously improving.

Next Time

Well, there you have it — probably way more than you ever wanted to know about arrow functions. I must say that this is one of my favorite features in ES2015, and I’ve taken to using arrow functions everywhere that makes sense.

In the next article, we’re going to be looking at another one of my favorite new ES2015 features: ES2015’s template literals, which give us string interpolation, multi-line strings, and more!

Like what I’m doing? Consider becoming a patron!

Creating articles, technical documentation, and contributing to open source software and the like takes a lot of time and effort! If you want to support my efforts would you consider becoming my patron on Patreon?

--

--

JavaScript fangirl, Technical writer, Mobile app developer, Musician, Photographer, Transwoman (she/her), Atheist, Humanist. All opinions are my own. Hi there!