JavaScript is full of mysteries, but none are more elusive than the 'this'
keyword. It’s the Sherlock Holmes of the coding world—always present, solving problems, yet often misunderstood. So, grab your magnifying glass, because today, we’re donning our detective hats to crack the case of the enigmatic 'this'
keyword. Even chameleons have fewer colors than the ever-changing, notorious 'this'
.
MDN Definition
The this
keyword refers to the context where a piece of code, such as a function's body, is supposed to run. Most typically, it is used in object methods, where this
refers to the object that the method is attached to, thus allowing the same method to be reused on different objects. The value of this
in JavaScript depends on how a function is invoked (runtime binding), not how it is defined. When a regular function is invoked as a method of an object (obj.method()
), this
points to that object.
The mystery of 'this'
keyword
const car = {
model: "Tesla Model X",
year: 2022,
makeSound: function(){
console.log(`${this.model} makes peep sound!`)
}
}
car.makeSound() // Tesla Model X makes peep sound!
In the above code snippet, everything seems straightforward. We have an object representing a car, with a couple of properties and a method. The method, makeSound
, logs a string that includes the car's model. When this method is called, it correctly logs Tesla Model X makes peep sound!
So far, so good. The method is referencing the model
property of the car
object, and when invoked, it behaves as expected. But let’s make things a bit more interesting.
const car = {
model: "Tesla Model X",
year: 2022,
makeSound: function(){
console.log(`${this.model} makes peep sound!`)
}
}
const carSound = car.makeSound
carSound() // undefined makes peep sound!
Wait, what just happened? It’s the same object, with just one alteration: we stored the reference to the makeSound
method in another variable, carSound
. But when we called carSound()
, instead of logging the car’s model, it logged undefined makes peep sound!
At first glance, this might seem puzzling, but this is where the 'this'
keyword steps into the spotlight. Let's take another example
class Car {
constructor(model){
this.model = model
}
buildCar(){
console.log(`Building ${this.model}, wait.`)
}
}
const teslaCar = new Car("Tesla")
car.buildCar() // Building Tesla, wait.
const buildCar = car.buildCar
buildCar() // Uncaught TypeError: Cannot read properties of undefined (reading 'model')
Again bewildered? this time the 'this'
has become undefined
somehow, and the code trying to find model property on the undefined
which is resulting in a TypeError
.
Quirks of JavaScript
In JavaScript, we don't have true standalone functions—everything is invoked in the context of some object. To illustrate this, let’s consider the following example where I’ve defined a simple function callMe
, which logs "Hello" to the console. When we call callMe()
, it correctly logs the message. Interestingly, if we call the same function on the window
object, it produces the same result.
function callMe (){
console.log("Hello")
}
callMe() // Hello
window.callMe() // Hello
But what’s really happening behind the scenes?
In JavaScript, if a function isn’t explicitly called on an object, it’s implicitly invoked in the context of the global object. In a browser environment, this global object is window
. So, when you define callMe()
at the top level, it’s actually being added as a method to the window
object. Thus, calling callMe()
is equivalent to calling window.callMe()
—both are pointing to the same function in the global scope.
This behavior highlights a fundamental aspect of JavaScript: functions are always methods of some object, even if that object is the global one. Understanding this can help demystify why certain functions behave the way they do, especially when dealing with the 'this'
keyword and scope.
In JavaScript, the value of 'this'
is determined by the context in which a function is called. When car.makeSound()
is called, 'this'
refers to the car
object, so this.model
correctly points to "Tesla Model X"
. However, when we store car.makeSound
in carSound
and call it as carSound()
, the context is lost. 'this'
no longer refers to the car
object but instead defaults to the global context (or undefined
in strict mode), leading to the unexpected output.
The 'Left of the dot' rule
function findThis(){
console.log('This is: ', this)
}
findThis() // This is: Window
window.findThis() // This is: Window
We've defined a simple function findThis
which prints the value of this
keyword, when called on its own it produces the window object. Implicitly findThis
been called on the window object. The general rule is the value of this
will be whatever is on the left side of the dot.
function findThis(){
console.log('This is: ', this)
}
const thisObj = {
findThis: findThis
}
findThis() // This is: Window
thisObj.findThis() // This is: {findThis: f}
As we can see, when we assign the reference of findThis
to thisObj
and then call the function through thisObj
, the value of 'this'
refers to thisObj
. This is because 'this'
now points to the object that invoked the method, which in this case is thisObj
.
However, you might wonder why an error occurs when we try to call a method from a class instance in the following example
class Car {
constructor(model){
this.model = model
}
buildCar(){
console.log(`Building ${this.model}, wait.`)
}
}
const teslaCar = new Car("Tesla")
const buildCar = car.buildCar
buildCar() // Uncaught TypeError: Cannot read properties of undefined (reading 'model')
When we call a method defined inside a class, we encounter different behavior. Here, when the buildCar
method is called without an object reference, the value of 'this'
becomes undefined
. As a result, trying to access a property like model
on undefined
throws an error.
This happens because when you isolate buildCar
and call it on its own, it loses the context of teslaCar
. Since there’s no object on the left side of the dot when the method is called, 'this'
doesn’t refer to the teslaCar
instance but instead becomes undefined
, leading to the error.
Manipulating the value of this
We’ve observed some quirky behavior with the 'this'
keyword, but there are ways to alter and control the context in which a function is executed.
Call
method
We use call
method to explicitly tell JavaScript to call some function on a particular object. It allows us to explicitly specify the object that should be used as the context for 'this'
. Let’s revisit our previous car example to see how this works:
const car = {
model: "Tesla Model X",
year: 2022,
makeSound: function(){
console.log(`${this.model} makes peep sound!`)
}
}
const carSound = car.makeSound
carSound() // undefined makes peep sound!
In this example, when we assign car.makeSound
to carSound
and call it, the context of 'this'
is lost, leading to the output: undefined makes peep sound!
. Now, let’s modify the code slightly and use the call
method:
const car = {
model: "Tesla Model X",
year: 2022,
makeSound: function(){
console.log(`${this.model} makes peep sound!`)
}
}
const carSound = car.makeSound
carSound.call(car) // Tesla Model X makes peep sound!
Voila! We’ve successfully manipulated the context of 'this'
. The call
method instructs JavaScript to use the provided object (car
in this case) as the context for 'this'
, regardless of the default behavior. But it doesn’t stop there; we can also use the call
method with a completely different object:
const tesla = {
model: "Tesla Model X",
year: 2022,
makeSound: function(){
console.log(`${this.model} makes peep sound!`)
}
}
const miniCooper = {
model: "Mini Cooper Clubman",
year: 2023
}
const carSound = tesla.makeSound
carSound.call(miniCooper) // Mini Cooper Clubman makes peep sound!
In this example, I’ve executed the makeSound
method, but with the context of the miniCooper
object. The call
method lets us explicitly define the context of 'this'
, rather than relying on JavaScript's default behavior.
Additionally, we can pass arguments to the call
method, allowing us to modify the behavior further:
const car = {
model: "Tesla Model X",
year: 2022,
makeSound: function(sound="peep"){
console.log(`${this.model} makes ${sound} sound!`)
}
}
const carSound = car.makeSound
carSound.call(car, "ZZZZ") // Tesla Model X makes ZZZZ sound!
Here, I’ve not only set the context of 'this'
to car
but also passed an argument to the makeSound
method, changing the sound it produces. The call
method is a powerful tool that allows us to control the execution context and ensure that 'this'
behaves exactly as we intend.
apply
method
This method is very similar to call
, with the key difference being how it handles arguments. While the call
method accepts arguments as a comma-separated list, apply
takes them as an array.
const car = {
model: "Tesla Model X",
year: 2022,
makeSound: function(sound="peep"){
console.log(`${this.model} makes ${sound} sound!`)
}
}
const carSound = car.makeSound
carSound.apply(car, ["ZZZZ"]) // Tesla Model X makes ZZZZ sound!
bind
method
While call
and apply
are powerful, they are often used less frequently compared to bind
. The primary reason is that call
and apply
execute the function immediately, whereas bind
works a bit differently. Instead of executing the function right away, bind
returns a new function with the specified this
context and optional arguments "baked in," allowing it to be called at a later time when needed.
const car = {
model: "Tesla Model X",
year: 2022,
makeSound: function(sound="peep"){
console.log(`${this.model} makes ${sound} sound!`)
}
}
const carSound = car.makeSound
const zzzSound = carSound.bind(car, "ZZZZ")
zzzSound() // Tesla Model X makes ZZZZ sound!
We use bind
to create a new function, zzzSound
, that is permanently bound to the car
object with the sound argument "ZZZZ"
. Unlike call
or apply
, bind
doesn’t execute the function immediately. Instead, it gives us a function that we can invoke later, with the this
context and arguments already set.
Utility-wise, bind
is incredibly versatile and widely used in scenarios like binding functions to timers, event listeners, or partially applying arguments to functions. It allows for greater control and flexibility, making it an essential tool in JavaScript development.
Arrow functions and this
keyword
Unlike traditional functions, arrow functions do not create their own this
value. Instead, they inherit this
from the surrounding lexical context. Let’s explore this concept with an example:
class Bike {
constructor(bike){
this.bike = bike
}
makeSound(){
setTimeout(function(){
console.log(`${this.bike} makes ZZZ sound!`)
}, 500)
setTimeout(() => {
console.log(`${this.bike} makes ZZZ sound!`)
}, 500)
}
}
const ninja = new Bike("Ninja")
ninja.makeSound()
In this example, we have a Bike
class with a makeSound
method that uses both a traditional function and an arrow function within setTimeout
.
When the traditional function is used:
setTimeout(function() {
console.log(`${this.bike} makes ZZZ sound!`);
}, 500);
The this
keyword inside the traditional function refers to the global object (or undefined
in strict mode), not the instance of Bike
. As a result, this.bike
is undefined
, and the output will be:
undefined makes ZZZ sound!
However, when we use an arrow function:
setTimeout(() => {
console.log(`${this.bike} makes ZZZ sound!`);
}, 500);
The arrow function does not have its own this
context. Instead, it captures this
from the surrounding lexical scope, which in this case is the makeSound
method of the Bike
instance. Therefore, this.bike
correctly refers to "Ninja"
, and the output will be:
Ninja makes ZZZ sound!
This difference in behavior illustrates why arrow functions are often preferred in situations where you need to preserve the this
context, such as in callbacks or event handlers.
Conclusion
We’ve covered quite a bit of ground, starting with an exploration of the mysterious 'this'
keyword and its sometimes-perplexing behaviour. We then dug deeper to understand why these quirks occur and how JavaScript handles the context of 'this'
. After unravelling the nuances, we examined various methods to manipulate the context of 'this'
during function execution. We delved into three versatile methods—call
, apply
, and bind
—each with its own distinct way of controlling the this
context. Through examples, we saw how these methods work and their practical applications.
Finally, we explored arrow functions and how they differ from traditional functions, particularly in how they handle 'this'
. We highlighted the benefits of arrow functions, especially their ability to retain the lexical this
context, making them incredibly useful in callbacks and event handlers. I’d like to give a special shoutout to Colt Steele, whose teachings were instrumental in helping me grasp these concepts.