Who’s ‘This’? A Quirky Exploration of JavaScript’s Most Puzzling Keyword

Who’s ‘This’? A Quirky Exploration of JavaScript’s Most Puzzling Keyword

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.