February 15, 2018 · Javascript ES6

ES6 arrow functions and `this`

According to the MDN documentation:

Two factors influenced the introduction of arrow functions: shorter functions and non-binding of this.

The first factor is pretty self explanatory. It's always nice to get rid of unnecessary boilerplate code. It makes code more expressive, elegant, and fun to write.

The second factor though, requires a bit more explanation. How exactly do arrow functions achieve non-binding of this, and why is that even significant? Answering this question is exactly what we're going to try to do in this post.

The Problem

Following the same tack as before, we're going to start by talking about how things used to be done in ES5. This will help motivate why arrow functions were created and what problems they're supposed to solve.

So on that note, take a look at the following code. What do you think it will print?

const greeter = {
  times: ["morning", "afternoon", "night"],
  addressee: "feller", 
  greet: function() {
    this.times.forEach(function(time) {
      console.log("Good " + time + ", " + this.addressee + "!");
    });
  }
};

greeter.greet();

Of course, we want it to print:

"Good morning, feller!"
"Good afternoon, feller!"
"Good night, feller!"

But what it actually prints is:

"Good morning, undefined!"
"Good afternoon, undefined!"
"Good night, undefined!"

This happens because the anonymous function passed into forEach isn't invoked off of any particular caller. This means that it will have its calling context set to the global object by default. If we're in the browser, the global object is window. So the anonymous function passed into forEach is actually trying to find an addressee property off of window. window doesn't have an addressee property. So we get undefined in each of the statements printed out.

This behavior is pretty inconvenient right? The problem is that we're losing access to the this of the outer scope. How can we fix this (no pun intended)?

How We Dealed in ES5

There are several ways to deal with this problem. I'm going to go over three common solutions. The first method utilizes lexical scoping.

Method 1: Lexical Scoping
const greeter = {
  times: ["morning", "afternoon", "night"],
  addressee: "feller", 
  greet: function() {
    const self = this;
    
    this.times.forEach(function(time) {
      console.log("Good " + time + ", " + self.addressee + "!");
    });
  }
};

greeter.greet(); 
// "Good morning, feller!"
// "Good afternoon, feller!"
// "Good night, feller!"

We simply initialize a new variable, call it self (or that), and let it point to this. Then, in the anonymous function passed to forEach, we use self to grab the original calling object. This method works, because it gets around the variable shadowing that was giving us problems before.

The second method uses bind to manually bind the calling context of the calling function.

Method 2: Using bind to manually bind this
const greeter = {
  times: ["morning", "afternoon", "night"],
  addressee: "feller", 
  greet: function() {
    this.times.forEach(function(time) {
      console.log("Good " + time + ", " + this.addressee + "!");
    }.bind(this));
  }
};

greeter.greet(); 
// "Good morning, feller!"
// "Good afternoon, feller!"
// "Good night, feller!"

bind is a method that is available on any function. It allows you to manually set the calling context of a function. Once set, that function's calling context will always stay the same, no matter how the function is called.

In the code above, we bind the calling context of the anonymous function passed to forEach to the this of the outer function. This method works, because the this that's passed as an argument to bind is the this that belongs to the outer greet method. In this way, the innermost this is successfully set to the original calling object.

The third method utilizes the optional thisArg that may be passed in as the second argument to forEach.

Method 3: Using optional thisArg of forEach
const greeter = {
  times: ["morning", "afternoon", "night"],
  addressee: "feller", 
  greet: function() {
    this.times.forEach(function(time) {
      console.log("Good " + time + ", " + this.addressee + "!");
    }, this);
  }
};

greeter.greet(); 
// "Good morning, feller!"
// "Good afternoon, feller!"
// "Good night, feller!"

forEach actually provides a way to set the calling context of the callback during its execution. So in order to utilize this extra functionality of forEach, we simply pass in the original calling object as the thisArg.

Enter Arrow Functions

What do you think of the proposed solutions we just went over? Do they seem inconvenient, even a bit hacky to you? Well, as you probably guessed by now, ES6 arrow functions provide a much more elegant solution to the problem.

// with ES6 arrow functions
const greeter = {
  times: ["morning", "afternoon", "night"],
  addressee: "feller", 
  greet: function() {
    this.times.forEach(time => {
      console.log("Good " + time + ", " + this.addressee + "!");
    });
  }
};

greeter.greet(); 
// "Good morning, feller!"
// "Good afternoon, feller!"
// "Good night, feller!"

Arrow functions don't have their own this. In other words, an arrow function doesn't have any calling context associated with it. When we refer to this in an arrow function, we're really just referencing the calling context of the surrounding scope. So in the code above, the this in the anonymous function passed to forEach just refers to the original calling object or greeter. Problem solved!

That's it!

That's pretty much all you need to know about ES6 arrow functions and this. I hope that sheds some light on why ES6 arrow functions were created.

There's a lot more to arrow functions than what we covered here though. I mean it's pretty weird that arrow functions have no this right? Well, there's actually a lot more weirdness to arrow functions that we haven't covered yet. The scope of this post was actually pretty narrow. That's because we only talked about arrow functions in relation to this. The plan for next time is to provide a more comprehensive overview of arrow functions in general. See you then!