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
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 button
s or form
s, 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.