javascript

Understanding "this" in JavaScript: An In-Depth Guide

author

Luis Paredes

published

May 23, 2023

When it comes to using JavaScript, one of the concepts that causes a lot of confusion is the keyword this and what it represents depending on the context where it is used.

In general, this is meant to be used a handy reference to be able to point to the current "object" when working in an object-oriented style.

As in JavaScript many values or entities can be treated as objects (and hence the phrase "everything is a object in JavaScript"), the variety of values this can point to seems to be endless. However, if you fully grasp the essentials of the Object and Function types as well as the essentials of OOP in JavaScript and the concept of the global scope, you'll be able to intuitively understand the value of this in any context.

In this article, we'll examine all these essentials so that by the end of it you can also claim that this doesn't confuse you anymore.

Let's dive in!

The Global scope

In JavaScript, the global execution context is known as global scope and this scope has an object containing members that make up the stardard library we have at hand without declaring or importing any other functionality.

Hence, standard JavaScript methods such as setTimeout and setInterval as well as globally available objects like Math or Object are actually just members of this global object.

As we know from the introduction, the keyword this points to the object of the context, therefore if we run the following code in the global scope of whatever execution context we're using, we're going to get the global object as the value of this:

console.log(this);

Global scope in the browser

Notice that in a browser environment of execution, the top-level scope (the scope of anything that is not inside an brackets or any JS construct) corresponds to the global scope, aand as a result if you run console.log(this) at the top-level, you'll get the global object of the environment.

The browser's global object is also known as the window object and it also contains DOM methods that available for the Window data type and can also be accessed using the keyword window:

console.log(window);

Global scope in Node

On the other hand, if you are working in a Node JavaScript environment, the top-level does NOT always represent the global scope.

When you write code at the top-level of a JavaScript file that will be executed by Node as a module you're essentially pointing to that module scope with this at the top-level, therefore, if you have an index.js containing only:

console.log(this);

and you run it like this:

node index.js

the logged value will be an empty object (because the module has no members) instead of the global object.

On the other hand, if you were to run the code like this:

node -e 'console.log(this)'

you'd get Node's global object logged to the terminal since the scope of execution is the global scope instead of the module scope.

The global object in Node is simply known by its generic name global, which is also the keyword that can be used in any scope to set and get its members:

console.log(global);

Objects

When using this as a value for a key in an object, it will always point to its enclosing scope, regardless of whether the object is nested.

Let's see it using the following example:

const obj = {
  a: this,
  b: this.a,
  c: {
    d: "d",
    e: this,
  },
};

console.log(obj.a);
console.log(obj.b);
console.log(obj.c.e);

If we run this in a Node environment as a module, the top-level scope will point to the module scope, and as a result we'll get this printed to the console

{} // the module scope object
undefined // {}.a
{} // the module scope object

On the other hand, if we were to run this code in the browser, the output would be:

window
undefined // there's no window.a
window

Functions

As functions define a scope, the behavior of the this value may vary depending on the context:

  • When declared at the top-level scope, the value is always the global object (even in a Node module)

    function bar() {
      return this;
    }
    
    console.log(bar()); // `global` in Node or `window` in browsers
    
  • When declared inside an object, the value will point to the object

    const obj = {
      name: this,
      foo: function () {
        return this;
      },
    };
    
    console.log(obj.obj()); // obj in both browsers and Node
    
  • When declared inside another function, the this in the inner function will point to the global object and the this in the outer function will point to whatever scope encloses it according to the three cases mentioned here

    const obj = {
      fn: function outerFunction() {
        console.log("outerFunction:", this); // outerFunction: obj
    
        const innerFunction = () => {
          console.log("innerFunction", this); // innerFunction: obj
        };
    
        innerFunction();
      },
    };
    
    obj.fn();
    

Arrow functions

An important caveat of arrow functions is that they don't have their own bindings for this, instead, they take their this value from their immediately enclosing scope.

If we rewrite the innerFunction in the example of the previous section, we'll notice how the value of its this is now obj, the reason being that obj is the this value of the enclosing scope (outerFunction):

Setting the this value with methods

The Function data type provides three methods that allow you to modify the value of this in the function in an imperative way: bind(), call() and apply().

bind() allows us to set (or bind) a specific this value for a function, providing a new function instance and without modifying the original function:

const obj1 = {
  name: "John",
  greet: function () {
    console.log(`Hello, ${this.name}!`);
  },
};

const obj2 = {
  name: "Anne",
};

const boundGreet = obj1.greet.bind(obj2);

obj1.greet(); // Hello, John!
boundGreet(); // Hello, Anne!

call() and apply() also allow us to set a specific this value, but calling the function instead of returning a new function to be called later. As these methods involve invoking the function, they also allow passing arguments using the following signature:

<function>.call(<thisArg>, <optionalArrayWithArguments>)
<function>.apply(<thisArg>, <optionalArg1>, ..., <optionalArgN>)

In this example you can see how to accomplish the same result using call() and apply():

function greet(message, emoji) {
  console.log(`${message}, ${this.name}! ${emoji}`);
}

const person = {
  name: "Anne",
};

greet("Hi", "😊"); // Hi, undefined! 😊
greet.call(person, "Hi", "😊"); // Hi, Anne! 😊
greet.apply(person, ["Hi", "😊"]); // Hi, Anne! 😊

Notice that these methods only make sense to be used with regular (non arrow) functions because arrow functions don't have a this binding of their own:

const greet = (message, emoji) => {
  console.log(`${message}, ${this.name}! ${emoji}`);
};

const person = {
  name: "Anne",
};

const boundGreet = greet.bind(person);

greet("Hi", "😊"); // Hi, undefined! 😊
boundGreet("Hi", "😊"); // Hi, undefined! 😊
greet.call(person, "Hi", "😊"); // Hi, undefined! 😊
greet.apply(person, ["Hi", "😊"]); // Hi, undefined! 😊

Classes and prototypes

JavaScript supports object-oriented programming through the use of prototypes and classes (which act as syntax sugar for the prototype mechanism).

The patterns associated with each style of OOP JavaScript are beyond the scope of this article, so we'll focus only on how this behaves when dealing with classes and prototypes.

Regardless of the syntax style you choose, whenever you create an instance of a class (an instance that uses the same prototype of the class), this always refers to the instance.

Here's an example to illustrate this using the class syntax:

class Person {
  constructor(name) {
    this.name = name;
  }

  greetAndSelfIntroduce() {
    console.log(`Hi! I'm ${this.name}`);
  }
}

const person1 = new Person("Louis");
const person2 = new Person("Susan");

person1.greetAndSelfIntroduce(); // Hi! I'm Louis
person2.greetAndSelfIntroduce(); // Hi! I'm Susan

and here's is the same example using one of the possible raw prototypal approaches:

function Person(name) {
  this.name = name;
}
Person.prototype.greetAndSelfIntroduce = function () {
  console.log(`Hi! I'm ${this.name}`);
};

const person1 = new Person("Louis");
const person2 = new Person("Susan");

person1.greetAndSelfIntroduce(); // Hi! I'm Louis
person2.greetAndSelfIntroduce(); // Hi! I'm Susan

Conclusion

In this comprehensive guide, we have explored the concept of this in JavaScript and its behavior in different contexts.

By understanding the essentials of this and the object-oriented nature of JavaScript, you are now equipped to confidently work with this in any context and harness its power in your JavaScript code!