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.