Context Is Key

"this" Keyword In-Depth

Todd Motto by Todd Motto Google Developer Expert Icon Google Developer Expert
JavaScript Icon

Learn JavaScript the right way

Premium JavaScript courses to skyrocket your skills to the top.

5x Courses: Basics, Masterclass, HTML5 APIs, DOM, Testing with Jest.

The this keyword is one of the most misunderstood parts of JavaScript, but is in fact quite a simple concept to understand.

this is a dynamic keyword that exists in all functions, and changes based on how that function is called. This is known as the “context”.

Using this is often seen in Object-Oriented Programming (OOP) as it allows us to bind properties and methods then reference them in the same class.

However, as this exists almost everywhere in JavaScript, its use cases go beyond OOP.

Let’s start with a familiar example of an event listener and examine its behavior:

const element = document.querySelector('div');

function handleClick() {
  console.log(this); // HTMLDivElement
  console.log(element); // HTMLDivElement
}

element.addEventListener('click', handleClick);

When the event listener fires, this will refer to the HTMLElement in the same way element does.

To explain this, we need to ask “who called me?”. Our addEventListener() is called by element, and therefore this refers to element.

The same applies with other constructs, like object literals:

const drink = {
  name: 'Lemonade',
  price: 299,
  getName() {
    return this.name;
  }
};

drink.getName();

The getName() method is called by drink, therefore this refers to the drink object.

Because of this, when we use this.name inside of getName(), the reference to drink is made and the property is found.

So, what about “normal” function calls? They are called by nothing right? Almost…

By default, the “global context” in JavaScript refers to the environment where the code is running, which in the browser means the window object.

Functions that are called in the global context are in fact “called” by the window, therefore it’s no surprise that this points to window:

function initialize() {
  console.log(this); // Window
}

initialize();

You can consider this the same as window.initialize().

In other environments, such as Node.js, this will point to globalThis, because in Node.js we are not inside a browser environment.

With so many nuances, it’s no surprise that this gives developers a hard time. It also behaves differently in JavaScript than it does in other programming languages.

To introduce a little further complexity, we need to consider JavaScript’s “strict mode”.

This is a fairly modern feature that allows the JavaScript compiler to make more assumptions about our code, and give us sensible errors.

To enable strict mode, we would declare 'use strict' at the top of a section of code, or for the contents of an entire file or program.

This now changes the this context to undefined.

function initialize() {
  'use strict';
  console.log(this); // undefined
}

initialize();

The change was made to stop constructor functions being used incorrectly, as developers would frequently “forget” to use the new keyword and as such any properties defined in constructors would instantly be bound to the window polluting the environment:

function Drink(name, price) {
  // this === window
  this.name = name;
  this.price = price;
}

Drink('Coffee', 399); // "new" keyword missing

In the above example, it’s easy to see why this is an issue. Of course, safety checks could be made in the constructor but this requires further knowledge. It’s why in strict mode this becomes undefined.

It’s also possible to change the this keyword, as it is a dynamic variable. We discussed the term “context” briefly at the beginning, and the context drives the value of this.

We can change it by using some built-in function methods like call(), apply() and bind().

Let’s assume the following example, notice how getName is not bound to either object:

const drink = {
  name: 'Coffee'
};

const food = {
  name: 'Bagel'
};

function getName() {
  return this.name;
}

But let’s say we want to use getName() to access the name property on each object, how could we?

This is where we can set the context of the function, thus changing the this value.

This is easily done using Function.prototype.call:

const drink = {
  name: 'Coffee'
};

const food = {
  name: 'Bagel'
};

function getName() {
  return this.name;
}

getName.call(drink); // 'Coffee'
getName.call(food); // 'Bagel'

Similarly, we can use Function.prototype.apply:

const drink = {
  name: 'Coffee'
};

const food = {
  name: 'Bagel'
};

function getName() {
  return this.name;
}

getName.apply(drink); // 'Coffee'
getName.apply(food); // 'Bagel'

The difference between call() and apply() are simply the style of which we pass arguments:

fn.call(context, arg1, arg2, arg3);
fn.apply(context, [arg1, arg2, arg3]);

The call() method accepts comma separated arguments, whereas apply() expects an array of your arguments - which are then transformed into comma separated parameters.

There’s one final piece of the puzzle, and that’s Function.prototype.bind - a slightly newer feature.

This allows us to setup the context ahead of time, instead of when we invoke the function:

const drink = {
  name: 'Coffee'
};

const getNameFromDrink = function getName() {
  return this.name;
}.bind(drink);

getNameFromDrink(); // 'Coffee'

Simply declare .bind() at the end of a function and it will return you a new function with the bound context. Unlike call() and apply(), you only bind the context and would pass any arguments as usual when calling your returned function. It’s a nicer API and was introduced in ES5.

One final thing to mention before we finish is how this is handled with arrow functions.

Arrow functions in fact have no this themselves, they inherit it from the parent scope. This means you have to be careful.

Taking our very first example, we would in fact see a very big difference of the this value when using an arrow function:

const element = document.querySelector('div');

const handleClick = () => {
  console.log(this); // Window
  console.log(element); // HTMLDivElement
};

element.addEventListener('click', handleClick);

With a function declaration the this context pointed to HTMLElement, but you can see how this now becomes Window - because the arrow function has no this and simply inherits from the next scope up.

You can see how a developer could easily introduce bugs if they refactored a function declaration into an arrow function.

There’s a lot to reveal about the this keyword, and I hope the examples in this guide have helped you understand what it is, why it exists and your expectations of its behavior.

Predictable and Testable

« Pure versus Impure Functions

Custom Class Objects

Constructor Functions and "new" »