Understanding this in javascript, is easy.

It is the great enemy of many people, especially beginners. We are talking about this in JavaScript, and the truth is that this reserved word in JavaScript is somewhat peculiar compared to most programming languages that use it, due to how JavaScript handles context.

But if you stop and think about it and understand the context, it's actually quite simple. In this article, we will try to explain it in a way that is easy to understand so that you can use it without getting confused and without delving into too many technicalities about the context; we'll leave that for another article.

One of the main situations in which people tend to get confused is this:

function foo() {
  return this === window;
}

function foo2() {
  return this === foo2
}

console.log(foo()); // Expected output: true
console.log(foo2()); // Expected output: false

As we can see, initially it may seem unintuitive. Many tend to think that in the previous code, this should refer to the function. However, if we run the code, we see that this actually refers to the window object. And why is that?

It's actually easy to understand if we run the following code:

function foo() {
  return this === window;
}

console.log(window.foo()) // Expected output: true

As we can see, the foo function is in the global scope of JavaScript, meaning it belongs to the window object. And what does this mean? Well, this refers to window. The keyword this always refers to the object to which it belongs (technically speaking, the instantiated object).

Let's do this now:

function foo() {  
  this.isWindow = function() {
    return this === window;
  }
  
  this.isMyObject = function() {
    return this === MyObject;
  }
  
  return this === window;
}

console.log(window.foo()) // Expected output: true

const MyObject = new foo();
console.log(MyObject.isWindow()); // Expected output: false
console.log(MyObject.isMyObject()); // Expected output: true

What has happened now? Well, foo is still a function that belongs to the global scope, meaning the window object. However, while in the first console.log we are calling foo from the global scope (window.foo()), right after that, we are creating an instance of foo with the new keyword. Therefore, if the owner of foo in the first case is the window object, in the second case, the owner is the instantiated object MyObject.

With this, we can already get an idea of what this is in JavaScript. To figure it out, we just need to ask ourselves, "Who is the owner object of this?" Knowing that a function is not an object unless it is instantiated with the new keyword.

Binding the scope (bind)

In many cases, we come across the need to bind the scope in a function, and for that, JavaScript offers us the possibility to do it with the bind method. Let's consider the following example:

function Car() {
  this.fuelLevel = "40 litters";
  
  setTimeout(function() {
    console.log(`Fuel level: ${this.fuelLevel}`)
  }, 500)
}

const MyCar = new Car(); // Expected output: 'Fuel level: undefined'

What's happening? Well, in this case, when we call setTimeout belonging to the global scope (window.setTimeout), we are passing it a callback function that will be executed after 500ms. Therefore, the callback function is within the scope of the window object. However, we can once again encounter something unintuitive if we do something like this.

Let's see two cases:

function Car() {
  this.fuelLevel = "40 litters";
  
  this.logFuelLevel = function() {
    console.log(`Fuel level: ${this.fuelLevel}`);
  }
  
  this.logFuelLevel();
}

const MyCar = new Car(); // Expected output: 'Fuel level: 40 litters'
function Car() {
  this.fuelLevel = "40 litters";
  
  this.logFuelLevel = function() {
    console.log(`Fuel level: ${this.fuelLevel}`);
  }
  
  setTimeout(this.logFuelLevel, 500)
}

const MyCar = new Car(); // Expected output: 'Fuel level: undefined'

As we can see, in the first example, this.logFuelLevel behaves as intuition tells us and returns the fuel level of the car. However, in the second case, it tells us that the fuel level is not defined. The reason is that in the second case, this.logFuelLevel is being called from setTimeout, which belongs to the global scope, and that's why it is not defined. The owner of this is not Car, but window where the setTimeout method is defined.

Let's try this:

var fuelLevel = "20 litters";

function Car() {
  this.fuelLevel = "40 litters";
  
  this.logFuelLevel = function() {
    console.log(`Fuel level: ${this.fuelLevel}`);
  }
  
  setTimeout(this.logFuelLevel, 500)
}

const MyCar = new Car(); // Expected output: 'Fuel level: 20 litters'

Now, by declaring the variable fuelLevel with var in the global scope, it is as if we were doing this:

window.fuelLevel = "20 litters";

Therefore, within the global scope (window), fuelLevel is now defined, and since setTimeout belongs to the global scope, it now tells us it is "20 liters". As we mentioned, this may seem unintuitive, but again, the question to ask is, "Who is the owner of setTimeout, the method we are calling?" The answer tells you who this refers to.

However, as we mentioned, JavaScript provides us with an option to bind functions using the bind method. Let's bind it in the following two examples:

function Car() {
  this.fuelLevel = "40 litters";
  
  setTimeout(function() {
    console.log(`Fuel level: ${this.fuelLevel}`)
  }.bind(this), 500)
}

const MyCar = new Car(); // Expected output: 'Fuel level: 40 litters'
function Car() {
  this.fuelLevel = "40 litters";
  
  this.logFuelLevel = function() {
    console.log(`Fuel level: ${this.fuelLevel}`);
  }
  
  setTimeout(this.logFuelLevel.bind(this), 500)
}

const MyCar = new Car(); // Expected output: 'Fuel level: 40 litters'

In these two cases, we are asking JavaScript to bind the function with the current this, which is the Car object. Therefore, this inside the setTimeout is now bound to Car.

Another example of reverse binding could be this:

var fuelLevel = "20 litters";

function logFuelLevel() {
  console.log(`Fuel level: ${this.fuelLevel}`);
}

const foo = logFuelLevel;
foo();  // Expected output: 'Fuel level: 20 litters' 
var fuelLevel = "20 litters";

function logFuelLevel() {
  console.log(`Fuel level: ${this.fuelLevel}`);
}

function Car() {
  this.fuelLevel = "40 litters";
}

const MyCar = new Car();

const foo = logFuelLevel.bind(MyCar);
foo();  // Expected output: 'Fuel level: 40 litters' 

In this case, we have a function that logs this.fuelLevel, this time outside of the class. As we can see in the first example, obviously, it tells us that the fuel level is 20 liters because this inside the logFuelLevel function belongs to the global scope, as we saw at the beginning of the article. But in the second example, we have bound the logFuelLevel function to the scope of the MyCar object, so the output is 40 liters.

In other words, in the first example, the owner of logFuelLevel is the window object, while in the second example, after binding it, the owner is the MyCar object.

"this" and the arrow functions

It seems like JavaScript folks wanted to make things complicated, but that's why they introduced arrow functions, aiming to simplify the this scope. Let's try this out:

var fuelLevel = "20 litters";

function Car() {
  this.fuelLevel = "40 litters";
  
  setTimeout(() => {
    console.log(`Fuel level: ${this.fuelLevel}`)
  }, 500)
}

const MyCar = new Car(); // Expected output: 'Fuel level: 40 litters'
var fuelLevel = "20 litters";

function Car() {
  this.fuelLevel = "40 litters";
  
  setTimeout(function() {
    console.log(`Fuel level: ${this.fuelLevel}`)
  }, 500)
}

const MyCar = new Car(); // Expected output: 'Fuel level: 20 litters'

As we can see now, it behaves as expected without the need for any binding because arrow functions execute in the scope where they are created. It could be said that they "inherit the this".

In the first of the two previous examples, we used an arrow function, which is created within the Car class, so the scope is "inherited" from where it was created. On the other hand, in the second example, we used a regular function, which respects the scope where it is executed, even though it was created within the Car class.

This simplifies code writing and saves us from the hassle of binding or thinking about the scope where the function is executed, as it always refers to the scope where it was created.

The old trick of self

If you visit older repositories, you will notice that in many cases, you'll see this assigned to a variable named self. This became a popular standard to avoid conflicts with the scope when using this. Let's see an example:

var fuelLevel = "20 litters";

function Car() {
  var self = this;
  this.fuelLevel = "40 litters";
  
  setTimeout(function() {
    console.log(`Fuel level: ${self.fuelLevel}`)
  }, 500)
}

const MyCar = new Car(); // Expected output: 'Fuel level: 40 litters'

As you can observe, self is always the this of the Car scope, and this way, confusion was avoided. However, as we have seen before, with arrow functions, this problem disappears, and the use of self becomes meaningless.

Classes, this, addEventListener and removeEventListener

Let's conclude the article with an example that often confuses beginners when working with classes.

Let's assume we have a Car class, continuing with the example, and we have an input[name=refuel] element that, when a value is entered, should add it to the car's fuel level using an addEventListener.

Here's how we can handle this scenario:

class Car {
  constructor(manufacturer, model, color) {
    this.manufacturer = manufacturer;
    this.model = model;
    this.color = color;
    this.fuelLevel = 10;
    
    document.querySelector("input[name=refuel]")
      .addEventListener("change", this.handleRefuel);
  }
  
  handleRefuel(ev) {
    const fuelToAdd = parseFloat(ev.target.value);
    this.addFuel(fuelToAdd);
  }
  
  addFuel(fuelToAdd) {
    this.fuelLevel += fuelToAdd;
  }
}

const MyCar = new Car();

This will not work; it will give us an error saying that addFuel is not defined. Why does this happen? As we explained at the beginning of the article, the addEventListener method belongs to the global scope, so this refers to window, and the addFuel method is not found in the global scope.

To solve this, we need to bind the object as follows:

class Car {
  constructor(manufacturer, model, color) {
    this.manufacturer = manufacturer;
    this.model = model;
    this.color = color;
    this.fuelLevel = 10;
    
    document.querySelector("input[name=refuel]")
      .addEventListener("change", this.handleRefuel.bind(this));
  }
  
  handleRefuel(ev) {
    const fuelToAdd = parseFloat(ev.target.value);
    this.addFuel(fuelToAdd);
  }
  
  addFuel(fuelToAdd) {
    this.fuelLevel += fuelToAdd;
  }
}

const MyCar = new Car();

This would work. But now, let's suppose that at some point we want to stop listening to the event, for example, if the input is removed from the DOM. For the example, we would like to stop listening to the input change after one minute, and we might want to do something like this:

class Car {
  constructor(manufacturer, model, color) {
    this.manufacturer = manufacturer;
    this.model = model;
    this.color = color;
    this.fuelLevel = 10;
    
    document.querySelector("input[name=refuel]")
      .addEventListener("change", this.handleRefuel.bind(this));

    setTimeout(() => {
      document.querySelector("input[name=refuel]")
        .removeEventListener("change", this.handleRefuel.bind(this));
    }, 60000);
  }
  
  handleRefuel(ev) {
    const fuelToAdd = parseFloat(ev.target.value);
    this.addFuel(fuelToAdd);
  }
  
  addFuel(fuelToAdd) {
    this.fuelLevel += fuelToAdd;
  }
}

const MyCar = new Car();

It may seem like it's fine, but in reality, this wouldn't remove the event because the removeEventListener method requires us to pass the exact same method (reference) to take effect. It should be something like this:

function handleRefuel() {
  const fuelToAdd = parseFloat(ev.target.value);
  console.log(`Added ${fuelToAdd}`)
}
    
document.querySelector("input[name=refuel]")
  .addEventListener("change", handleRefuel);

setTimeout(() => {
  document.querySelector("input[name=refuel]")
    .removeEventListener("change", handleRefuel);
}, 60000);

In this case, the removeEventListener would work because the reference to handleRefuel is exactly the same. But in the case of our class, it wouldn't work because the bind method doesn't return the method reference of our class this.handleRefuel, but rather, without going into much detail, a copy of it. We will discuss references in another article. Therefore, in reality, the method passed to addEventListener, even though it may seem so, is not the same as the one passed to removeEventListener. To solve this, we can resort to the old trick of using self:

class Car {
  constructor(manufacturer, model, color) {
    this.manufacturer = manufacturer;
    this.model = model;
    this.color = color;
    this.fuelLevel = 10;
    
    const self = this;
    
    document.querySelector("input[name=refuel]")
      .addEventListener("change", self.handleRefuel);

    setTimeout(() => {
      document.querySelector("input[name=refuel]")
        .removeEventListener("change", self.handleRefuel);
    }, 60000);
  }
  
  handleRefuel(ev) {
    const fuelToAdd = parseFloat(ev.target.value);
    this.addFuel(fuelToAdd);
  }
  
  addFuel(fuelToAdd) {
    this.fuelLevel += fuelToAdd;
  }
}

const MyCar = new Car();

Alternatively, we can use a more modern approach by utilizing arrow functions, which, as mentioned before, maintain the scope where they are created. We can return the handleRefuel method from our class as follows:

class Car {
  constructor(manufacturer, model, color) {
    this.manufacturer = manufacturer;
    this.model = model;
    this.color = color;
    this.fuelLevel = 10;
    
    document.querySelector("input[name=refuel]")
      .addEventListener("change", () => this.handleRefuel);

    setTimeout(() => {
      document.querySelector("input[name=refuel]")
        .removeEventListener("change", () => this.handleRefuel);
    }, 60000);
  }
  
  handleRefuel(ev) {
    const fuelToAdd = parseFloat(ev.target.value);
    this.addFuel(fuelToAdd);
  }
  
  addFuel(fuelToAdd) {
    this.fuelLevel += fuelToAdd;
  }
}

const MyCar = new Car();

Both solutions work because in both cases we are respecting the scope, and therefore, this refers to our MyCar object.