Tutorials Books Videos Forums

Change the theme! Search!
Rambo ftw!

Customize Theme


Color

Background


Done

Table of Contents

Making Sense of "this" and More in JavaScript!

by kirupa   |   filed under JavaScript 101

What is this mysterious this keyword that keeps showing up? Let's find out and get really good at using it!

In English, there are many situations where you need to refer to yourself. I am hungry. This teleportation device belongs to me. I don't know who microwaved mustard. I digress.

In JavaScript, things aren't too different. We will write or encounter code where we need to refer to the current object in a very general way. The way we get a reference to this object is by the appropriately named this keyword. We've seen this keyword a few times already, but now its time for us to look deeper into what this actually is and how to work around some quirks where what we think our current object should be and what this actually references don't match.

Onwards!

This 101

When our JavaScript code runs, it always runs inside some context. We saw a bit of this when we looked at variable scopes much earlier. Depending on where our code is defined, it could run fully localized inside a function. It could run globally at the Window scope. Our code could also be constrained to a particular object such as inside a class or object definition. The way we can figure out what the context our code is operating is by referring to the this keyword.

For example, let's say we print the value of this from our global context:

console.log(this); // Window

What we will see printed to our console is our Window object. Now, let's say we are inside an Object and print the value of this from inside a property:

let myObject = {
  whatIsThis: function () {
    console.log(this);
  }
};
myObject.whatIsThis(); // Object

The value of this references the myObject object it is contained inside. If we didn't have a way to use this, there are many things we simply can't do. For example, let's say we want to reference a property within our object as shown below:

let myObject = {
  name: "Iron Man",
  whatIsThis: function () {
    console.log(name); // won't work!
  }
};
myObject.whatIsThis(); // undefined

We can't do this. But, if we rely on this, we can totally pull this off:

let myObject = {
  name: "Iron Man",
  whatIsThis: function () {
    console.log(this.name); // yay!
  }
};
myObject.whatIsThis(); // "Iron Man"

Most recently, we saw the this keyword being used extensively when working with classes. Take a look at the following example:

class Fruit {
  constructor(name) {
    this.name = name;
  }
  getName() {
    return this.name;
  }
}

let apple = new Fruit("Apple");
console.log(apple.getName()); // "Apple"

let orange = new Fruit("Orange");
console.log(orange.getName()); // "Orange"

When we use the this keyword as part of creating objects using the class syntax, this references the object instance it is bound to such as apple or orange. Because this behaves this way inside our class definition, the appropriate name value gets returned.

When This Just Ain't Right

Now that we have looked at cases where this behaves exactly like we would expect, brace yourself. This is going to be a sordid tale involving variable scopes and the value of this. These are two topics that, on the best of days, can make anyone's head spin. The best way to understand how the way this behaves can cause some major headaches is to look at an example. The example looks as follows:

let counter = {
  initial: 100,
  interval: 1000,
  startCounting: function () {
    setInterval(function () {
      this.initial++;
      console.log(this.initial);
    }, this.interval);
  }
}
counter.startCounting();

We have an object called counter, and it has the initial, interval, and startCounting properties. The property that we want to focus on is startCounting, and it represents a function whose body is a setInterval function:

let counter = {
  initial: 100,
  interval: 1000,
  startCounting: function () {
    setInterval(function () {
      this.initial++;
      console.log(this.initial);
    }, this.interval);
  }
}
counter.startCounting();

When someone calls startCounting, the idea is for us to increment the value of initial at a rate specified by the interval property.

Thinking out loud, when we call counter.startCounting(), the value of initial is initially (ha!) 100. After 1000 milliseconds (as specified by the interval property) elapses, the value of initial is incremented by 1 to be 101. After another 1000 milliseconds, the value of initial becomes 102. You get the picture.

Now, if we test this code, what do you think we are going to see? What will the console.log statement inside our interval loop show when it prints the value of initial every 1000 milliseconds? As it turns out, what we will see is not a nice set of numbers increasing by 1 starting from 100. Instead, what we will see is NaN being printed over and over again:

Why is this the case? We can see more details if we add a few more console.log statements to see what the value of this is inside thestartCounting and setInterval functions:

let counter = {
  initial: 100,
  interval: 1000,
  startCounting: function () {
    console.log("startCounting:");
    console.log(this);
    setInterval(function () {
      console.log("setInterval:");
      console.log(this);
      this.initial++;
      console.log(this.initial);
    }, this.interval);
  }
}
counter.startCounting();

If we run this code, we'll see our additional information being printed to the console. The value of this under startCounting will refer to the counter object:

The value of this inside oursetInterval function will refer to the window object:

Here is where things are problematic. You may think that setInterval would inherit the value of this from its outer environment as defined by startCounting, but that isn't the case. The reason is that traditional functions don't behave in this seemingly logical way. They define their own value for this and that is always going to refer to the context they are being used in.

Our anonymous function inside our setInterval doesn't get created when our counter object is initialized. It gets created only when we call the startCounting method:

counter.startCounting();

This call lives in the context of the window object. When startCounting is invoked and the anonymous function is created, the this.initial call is looking for the value of initial on the window object. That property doesn't exist there, and that is why trying to increment this non-existent variable gives us a NaN.

Using a Redefined Version of the this Keyword

One approach to get us out of this gully is to store the value of this and pass the stored this value into our anonymous function. Take a look at the following:

let counter = {
  initial: 100,
  interval: 1000,
  startCounting: function () {
    let that = this;
    setInterval(function () {
      that.initial++;
      console.log(that.initial); // works
    }, this.interval);
  }
}
counter.startCounting();

Notice that we introduced a that variable in the startCounting context to store a local reference to this. Inside our anonymous function, we use that where we earlier used this. Because that is properly storing a version of this that is tied to our counter object, our use of that.initial properly resolves to the correct value. If we run this code, we'll see the output of the initial properly incrementing when we examine our console:

All of these mentions of this and that in this explanation can be a bit confusing, but what we are doing is just redefining the value of this into a local variable. You don't have to use the variable name that either. You can call it anything you want:

let counter = {
  initial: 100,
  interval: 1000,
  startCounting: function () {
    let baconAndEggs = this;
    setInterval(function () {
      baconAndEggs.initial++;
      console.log(baconAndEggs.initial); // undefined
    }, this.interval);
  }
}
counter.startCounting();

I won't say that this variable name for the redefined this makes our code run better, but it certainly does make it run as part of a complete breakfast!

Arrow Functions and their Lexical Scopeness

We just saw one approach of addressing this problem by storing the value of this and using this stored value in place of the actual this. There is an arguably better approach. Let's say we replace our anonymous function inside our setInterval with an arrow function:

let counter2 = {
  initial: 100,
  interval: 1000,
  startCounting: function () {
    setInterval(() => {
      this.initial++;
      console.log(this.initial); // works
    }, this.interval);
  }
}
counter2.startCounting();

When we run this code, what gets printed this time around is 100, 101, 102...and so on just like we want. No mess. No fuss. No that and this confusion. Why does an arrow function work here while traditional functions require extra gymnastics?

The reason is that arrow functions do not define their own this value. Instead, they inherit the value of this from where their code is defined in the document. Another way of saying this is that the this value inside arrow functions is determined by its surrounding scope, more formally called the lexical scope. This is in contrast to traditional functions whose this value comes from the context in which they are used in. This is a subtle difference with a huge (and beneficial) change in behavior.

Note: Some Other Things Arrow Functions Don't Have

Beyond not having their own this value (which they inherit from their surroundings), arrow functions have no constructor or prototype properties. They also don't support the bind, call, and apply methods. If you have existing code that relies on these properties and methods, your best bet is to stay with traditional functions or learn how to use arrow functions and adapt your code accordingly.

One Method to Bind Them All

We have seen several cases so far where the value of this inside a function isn't quite what it needs to be. Instead of letting the environment dictate the this value, what if we had a magical way to just tell a function (very sternly, yet kindly) what its value of this should be? That magical way happens to be something the bind method provides. The way bind works is a bit mysterious, but what we need are just two things to use it:

  1. A function whose value of this we want to set
  2. The star of this section, the bind method itself

If we had to illustrate this up, we would see something that looks as follows:

What we do next is where the magic part comes in: we take our regular function, call bind from it, and provide the value of this that we want our regular function to use. The syntax looks a bit as follows:

let boundFunction = myRegularFunction.bind(valueOfThis);
boundFunction();

When called, what bind creates is a new function known as a bound (or exotic) function where it wraps our regular function and injects the value of this that it needs to have to behave correctly:

Getting back to our example from earlier, below is how we use bind to ensure our setInterval gets the correct value for this:

let counter3 = {
  initial: 100,
  interval: 1000,
  startCounting: function () {
    setInterval(function() {
      this.initial++;
      console.log(this.initial); // works
    }.bind(this), this.interval);
  }
}
counter3.startCounting();

When we run this code, we'll see our console printing 100, 101, 102, and so on just like how we expect. As the highlighted line shows, we have our anonymous function tagged with the bind method, and the argument we pass to bind is the value of this from the same context our setInterval is defined in. This ensures that any code inside our anonymous function that references this (such as this.initial++) gets the value of this from what we passed in via bind as opposed to inferring the window object from its environment...which we don't want!

Conclusion

One of the most frustrating parts of writing or reading JavaScript is figuring out what in the world this is referring to at any given time. Unlike other JavaScript quirks that have managed to be replaced by better solutions over the years, there are no signs that we'll ever stop using this. The best we can do is take the time to better understand how this works and prepare ourselves for what surprises this will have in store in the future!

Just a final word before we wrap up. If you have a question and/or want to be part of a friendly, collaborative community of over 220k other developers like yourself, post on the forums for a quick response!

Kirupa's signature!

The KIRUPA Newsletter

Thought provoking content that lives at the intersection of design 🎨, development 🤖, and business 💰 - delivered weekly to over a bazillion subscribers!

SUBSCRIBE NOW

Creating engaging and entertaining content for designers and developers since 1998.

Follow:

Popular

Loose Ends

:: Copyright KIRUPA 2024 //--