Lesson Tuesday

Arrow functions are one of the most popular and useful new features in ES6. In fact, we've already been using arrow functions with Jest. Now it's time to delve more deeply into why they are useful and how we can use them in our code. There are a few reasons why arrow functions are so popular. We'll be focusing primarily on one of these reasons: arrow functions change the way this is bound inside of a nested function.

this can be a very confusing topic in JavaScript, especially for beginners, and it's not always clear what this is bound to. You may have dealt with this issue in Introduction to Programming; it's very common (and frustrating) for new developers to try to use this, only to find that it's undefined.

The best way to simplify this is to think of it within the context of object-oriented programming. Specifically, we should probably only use this if we're calling a function on a specific object.

In fact, this is what we commonly do inside of classes. (If classes still feel like a weird new concept, you can also think of this as being similar to what we've done with prototypal functions in the past.)

Let's look at some code that isn't going to work as expected. The reason, as you might guess, is related to this. Go ahead and put the following code in the console:

class Box {

  constructor() {
    this.stuff = []
  }

  addJunk(array) {
    console.log(this);
    array.forEach(function(thing) {
      this.stuff.push(thing);
    });
  }
}

In this example, we create a Box class. It has one property: this.stuff, an empty array.

Next, we have Box.prototype.addJunk(array). It seems like a straightforward method - we pass in an array and then for each element in the array, we push that element into this.stuff.

Well, if we try it in the console, we'll see that Box.prototype.addJunk(array) doesn't work correctly. Add this code in the console:

let box = new Box();
box.addJunk(["broken pencils", "busted rubber bands", "checkers pieces"]);

We'll get the following console.log() message:

Box {stuff: Array(0)}

This indicates that this is what it's supposed to be.

But we'll also get the following error:

Uncaught TypeError: Cannot read property 'stuff' of undefined

What just happened? When our code tried to read this.stuff inside the loop, it threw an error because this is undefined.

And why would that happen? We just used a console.log(this) to verify that this is exactly what we thought it was.

However, we used console.log(this) outside of the loop. This is a weird thing about JavaScript. We entered a loop and this lost its scope. The default for JavaScript is for this to default to the window object if it hasn't been defined - but that's not doing anything for us here.

In the method Box.prototype.addJunk(), the value of this is our console.log() statement is exactly what it should be - the thing our method is called on. So if we call box.addJunk(["broken pencils", "busted rubber bands", "checkers pieces"]), this is equal to box.

However, once we are inside Array.prototype.forEach() (our callback function), this no longer refers to the box. Instead, JavaScript is looking for a window object that doesn't exist.

It's not ideal at all - and traditionally JavaScript developers dealt with the issue by manually binding this. There are several ways to do this but this is the easiest approach (before arrow notation came along, that is):

addJunk(array) {
  let that = this;
  array.forEach(function(thing) {
    that.stuff.push(thing);
  });
}

We declare a variable named that inside our function and make it the value of this. The inner function has access to that - which is just a reference to this, really. This way, we can push stuff into the box.

This approach is a hack, though, and JavaScript's default behavior really isn't great. Do we really want to use the above hack any time there's a callback inside another function?

Fortunately, we can fix this problem by using arrow notation. With arrow notation, this remains bound to the object defined in the outer function.

addJunk(array) {
  array.forEach((thing) => {
    this.stuff.push(thing);
  });
}

What we've done here is taken away the function() and replaced it with () =>. (Parameters still go inside the parentheses as needed.)

So when should we use arrow notation instead of the notation we've used in the past? Well, it's becoming increasingly common to always use arrow notation regardless of whether you're concerned about binding this or not. That's because arrow syntax is more concise and because its behavior is predictable and helps ensure that our code works how we'd expect it to.

So how exactly does this syntax look in situations other than with Array.prototype.forEach()?

Here are some examples:

Unnamed Functions

Here's an unnamed function without arrow notation:

function (name) {
  return "hi " + name;
}

Here's the same function with arrow notation:

(name) => {
  return "hi " + name;
}

Named Functions

How about for a named function? This is how we've done it so far:

function greeting(name) {
  return "hi " + name;
}

To do this with arrow notation, we need to save the function in a variable like this:

const greeting = (name) => {
  return "hi " + name;
};

This may look pretty strange at first - but it will become more familiar over time. We will cover how functions can be saved in variables when we learn about functional programming in the React course. If you want to know the terminology now, when a function is saved in a variable, it's known as a function literal.

You can continue using function () instead of () => if you prefer. The one exception is when you need to bind this to an inner function. In that case, you should use arrow notation instead.

Syntactic Sugar with Arrow Notation

It's also possible to use arrow notation to make our code even more concise, though it will look even more abstract as a result. Specifically, if the body of the function is a single line, we can omit both the brackets and the return keyword. Let's take a look:

const greeting = name => "hi " + name;

We even omit the parentheses around the example above. This is very concise but it can be confusing for new developers - so don't use this syntax unless you feel very comfortable with it. In fact, there are a few gotchas with this syntax.

If the function has two arguments, you can't omit the parentheses:

const greeting = (greeting, name) => greeting + " " + name;

And if the code is multi-line, you can't omit the brackets or the return keyword:

const greeting = (greeting, name) => {
  const uppercasedGreeting = greeting.toUpperCase();  
  const uppercasedName = name.toUpperCase();
  return uppercasedGreeting + " " + uppercasedName;
};

This is a contrived example because we could easily reduce this function to one line. The point is that as soon as we have multiple lines in the body of a function using arrow notation, we need to use brackets and the return keyword.

For more information on arrow notation, see Arrow function expressions in the Mozilla documentation.

You will see arrow notation a lot in documentation - and the longer you code in JavaScript, the more likely you are to use it regularly. We recommend getting very familiar with arrow notation and then using it entirely once you are comfortable with it.

Lesson 38 of 48
Last updated April 8, 2021