Shades of 'this' in JavaScript

'this' keyword is like a chameleon - it adapts to its surroundings!

Shades of 'this' in JavaScript

Overview

  • Depending on where it is used, this behaves differently: in the global space¹, inside a regular function², within an object's method³, in an arrow function⁴, a callback function⁵, or an event handler⁶.

  • There are nuances between strict and non-strict modes², adding a little complexity.

  • We can explicitly pass in a this context to call() or apply()⁷ methods. There's an exception with bind()⁸ method which can set the value of a function's this regardless of how it is invoked which requires us to trace its origin.

Let's break it all down and understand each point in plain and simple terms!

1. In global space

In the global space, this takes on different identities (i.e., global object) depending on where your JavaScript is running.

  1. In web browsers, this refers to window object,

  2. In Node.js, this refers to an object called global.

console.log(this);  //Window{..} or global object

2. In a function

In functions, the this keyword initially points to undefined. Yet, it depends on the presence of strict mode.

In strict mode ("use strict"), this remains the same, i.e., undefined.

"use strict";
function foo() {
  console.log(this);    //undefined
}

foo();

Note: If you call the function as window.foo(), even in strict mode, 'this' will refer to the window object.

"use strict";
function foo() {
  console.log(this);  // Window{..}
}

window.foo();

Now, in non-strict mode, this refers to the global object due to default binding or what we call "this substitution."

What is this substitution? In non-strict mode, when the value of this is either undefined or null, JavaScript replace its value with the global object!

// non-strict mode
function bar() {
  console.log(this);  // Window{..} or global object
}

bar();

3. In an object method

What is the difference between a function and a method?

A function is a standalone block of code that can be invoked independently, whereas a method is a function associated with an object that can be invoked using the object it belongs to.

In the case of object method, the this refers to its owner object (Object method binding).

const employee = {
  fullName : 'Robin Kataria',
  getName : function() {
        console.log(this.fullName);
  }
};

employee.getName();

In the above example, this is like a mirror reflecting to the employee object itself.

4. In an arrow function

According to the MDN definition, "...and, arrow functions don't provide their own this binding (it retains the this value of the enclosing lexical context)."

Now, "enclosing lexical context" might sound a bit fancy, but it simply refers to the environment in which a function is defined. This environment allows a function to access variables from its outer (enclosing) scope - this is known as lexical scoping.

Let's look at two examples:

Example 1: Arrow function inside global space

const foo = () => {
  console.log(this);    //global object
}

Example 2: Arrow function nested inside an object

const myObject = {
  myMethod : function() {
    const arrowFunction = () => {
      console.log(this); // myObject {myMethod: f}
    };
    arrowFunction();
  },
};

myObject.myMethod();

In this example, this is invoked inside arrowFunction which is defined in an Object method myMethod. Following the definition, this points to its enclosing lexical context, which is the method myMethod.

As myMethod is an object method, this ultimately refers to the myObject object itself.

5. Advance case of this keyword

Let's put together everything we've learned so far and try to guess the output of the following code:

const carOwner = {
  ownerName: "Robin",
  cars: ["Sedan"],
  printCars: function() {
    this.cars.forEach(function(car) {
      console.log(`${this.ownerName} owns a ${car}`);
    });
  }
};

carOwner.printCars();

In line 6, when we use console.log(`${this.ownerName} owns a ${car}.`), the question arises: Does this refer to the carOwner object since it's inside a method of that object?

Instead of seeing Robin owns a Sedan, the correct output is undefined owns a Sedan.

The reason is that this.ownerName is undefined because this is referring to the global object (window), and window.carOwner is undefined.

You might be wondering, isn't the function (car) {...} inside the object method and why it isn't referring to the Object, as explained in Point 3?

The key is that the this is inside a callback function, and a callback function is just a regular function. In non-strict mode, this inside a regular callback function refers to the global object.

To address this issue, many developers prefer using Arrow Functions as callback functions. As mentioned in Point 4, arrow functions do not provide their own this binding (they retain the this value of the enclosing lexical context).

So, if we replace the regular callback function with an Arrow Function, we get the expected result.

const carOwner = {
  ownerName: "Robin",
  cars: ["Sedan"],
  printCars: function() {
    this.cars.forEach((car) => {
      console.log(`${this.ownerName} owns a ${car}`);
      //Robin owns a Sedan
    });
  }
};

carOwner.printCars();

For further details, refer to Youssef Zidan's blog on this keyword.

6. Inside an event handler

<button onclick="alert(this)">Click here</button>

In this scenario, this points directly to the HTML element that triggered the event. It refers to the button itself. When you click the button, the alert message will display 'HTMLButtonElement.'

With this, we can explore the element's properties. For instance, this.tagName will give the tag name of the button.

7. Explicit function binding

By using .call() or .apply(), we can explicitly pass in a this context to a function. This is like providing instructions, ensuring that the function operates with a specific object.

Consider the example below, where we have two persons:

const car = {
  displayInfo: function() {
    return this.make + " " + this.model;
  }
}

const myCar = {
  make: "Hyundai",
  model: "Verna",
}

Now, with the magic of call(), we can call the car.displayInfo method with myCar as an argument.

// Return "Hyundai Verna":
car.displayInfo.call(myCar);

In this scenario, this within the displayInfo method refers to myCar, even though displayInfo is originally a method of car. It's like giving a method a temporary pass to work with a different object.

8. Function Borrowing

With the bind() method, an object can borrow a method from another object.

In the code snippet below, two objects, carOwner and carDriver, are defined. The carDriver object has similar properties as carOwner but lacks the method getOwnerFullName.

const carOwner = {
  ownerName: "Robin",
  ownerSurname: "Kataria",
  getOwnerFullName: function () {
    return this.ownerName + " " + this.ownerSurname;
  }
}

const carDriver = {
  ownerName: "John",
  ownerSurname: "Doe",
}

let driverFullName = carOwner.getOwnerFullName.bind(carDriver);

The last line uses the bind method to create a new function driverFullName that is essentially a reference to getOwnerFullName method of carOwner, but with carDriver as the context (this value). This allows driverFullName to use the method from carOwner with the data from carDriver.

And, it's a wrap!

Thanks for stopping by and spending your time reading this article :)

List of references: w3schools, mdn web docs, Akshay Saini, Youssef Zidan, zcaceres