Back to Basics: Foolproof Tools to Conquer "this" in Javascript

Understanding the dark magic of function context in Javascript and Typescript

I've been thinking about this a lot recently because I've been messing around with a lot of chained callback functions in my web code. This is a good opportunity to go back to basics and recap how this works in Javascript and what tools exist to tame its quirks.

For new developers coming from a more typically object-oriented language like Java or Swift, Javascript's weird use of the this keyword is a trap waiting to crash your code at any moment. This is especially dangerous if you're using React's class components, where you're often defining methods on your class to act as a callback handler. If you blindly assume that this is going to behave the way you've come to expect, you're gonna have a bad time. So, let's understand this enemy so we can learn how to fight it:

What is this

What's This

Let's start with the basics of how we expect this to work under the best circumstances:

'use strict';

class Person {
  name;

  constructor(theirName) {
    this.name = theirName;
  }

  introduce() {
    console.log("Hello I'm " + this.name);
  }
}

const william = new Person("Bill");
william.introduce(); // Prints out "Hello I'm Bill"

This is pretty straightforward: there is a class of object called Person. Each Person remembers a variable called name and has a method called introduce. When you call introduce on a person it looks at that person's name and prints an introduction. So, this is a reference to the object whose instance of introduce we're looking at, right?

Well, not quite. Take a look at this:

// Continued from above

// This doesn't RUN william's introduce function,
// it makes a REFERENCE to it
const introduceWilliam = william.introduce;

// Because it's a reference to a method that worked,
// we might assume the reference will also work but...
introduceWilliam();
// Uncaught TypeError! Cannot read property 'name' of undefined

Now we've delved below the calm surface into the dark depths of a functional programming language written in the 90's.

You have to remember that as far as Javascript is concerned functions are just another kind of object. They can be stored, passed around, and executed anywhere.

When you call someThing.someFunc(), Javascript parses that you want to execute the instructions in someFunc in the context of someThing. That is to say, set this to someThing and then execute the instructions.

But if you make a reference to someFunc, you could execute it anywhere. Above, we called it in the global context, which leaves this as undefined when you're in strict mode. You can even use the function's call or apply methods (functions on a function!) to provide any context and args you desire.

Let's write some mildly horrifying code to demonstrate this:

// Still using william from above
const william = new Person("Bill");
// Make a reference to william's introduce method
let introduce = william.introduce;

// Make an unrelated object - Bagel the Beagle
const puppy = { name: "Bagel", breed: "Beagle" };
// Run function with manual `this` - Dogs can talk now
introduce.call(puppy); // Prints "Hello I'm Bagel"

Taming this Beast

This this is incredibly, and often unnecessarily, powerful. Like many incredibly powerful things, it is also incredibly dangerous. Because of how often we pass around references to functions - to use as callbacks for buttons or forms, for example - the unbound nature of this is just lying in wait to trip you up.

So how do we tame this? I could shake my cane at you and croak "Well, back in my day..." but the truth is that the ES5 and ES2015 revisions to Javascript gave us everything we need to clamp down wandering this values:

Function.prototype.bind()

Added in ES5, the first tool we got was the bind() function, a standardization of this hacks that the various utility libraries of the 2000's had innovated.

// Bind this reference to introduce so this is ALWAYS william.
let alwaysIntroduceWilliam = william.introduce.bind(william);

alwaysIntroduceWilliam(); // Prints "Hello I'm Bill"
alwaysIntroduceWilliam.call(puppy); // Prints "Hello I'm Bill"

bind does what it says on the tin. It binds the function to a chosen this - ensuring that the instructions inside are always run in the context we choose. Here you can see that even if we try to use call to set a different this, the bind overpowers and we're always introducing william. This was a great first step towards fixing this, but these days is less commonly used because of...

Arrow'd =>

Added in ES2015, arrow functions gave us (almost accidentally) the most common way of fixing this to the value that we expect. This is because an arrow function creates a closure over the context in which it was defined. What that means is that all the variables referenced inside the arrow will always reference the same references in memory as when the arrow was first parsed.

This is incredibly useful for capturing local variables so that they can be used later, but it has the added benefit of capturing the value of this that was set when the arrow was defined. And, since this is (basically) always going to be the object being created during construction, we can use arrow functions to make methods where this will behave exactly like we expect:

// Rewriting Person with arrows
class ArrowPerson {
  name;

  constructor(theirName) {
    this.name = theirName;
  }

  introduce = () => {
    // The arrow captures `this` so it is actually a
    // reference to THIS Person.
    console.log("Hello I'm " + this.name);
  }
}

const arrowBill = new ArrowPerson("Arrow Bill");
arrowBill.introduce(); // "Hello I'm Arrow Bill"

// Now `this` is fixed even as we pass the function around:
const introduceRef = arrowBill.introduce;
introduceRef(); // "Hello I'm Arrow Bill"
introduceRef.call(puppy); // "Hello I'm Arrow Bill"

this all makes more sense now

I hope you understand this a little bit better now. To be honest, I think I understand it better just from writing this all out. And, because the Javascript this can affect all your code that transpiles into Javascript, hopefully this will also help you understand the twists and turns of function context in other languages like Typescript.

If you have any questions about this, drop them in the comments below. Even after years writing for the web, I'm still learning so I'm sure there are terrible dangers and cool facts about this I forgot or don't yet know.

Comments (1)

Joyancefa's photo

Very nice explanation 🎉