by Gordon Zhu, May 2023
[If you prefer video, see the YouTube version of this post.]
In 2013, I found myself on a team that was building one of the largest AngularJS applications in the world at the time. And there was no way to avoid this
—it was everywhere.
Now that I had no choice, I finally decided to tackle this
once and for all—my previous hazy understanding would no longer cut it. I wanted a foolproof process that would allow me to methodically analyze any usage of this
with pinpoint precision. I had no patience for overly complicated processes that included irrelevant details (this was my experience with documentation and tutorials).
After several long sessions, I designed my own system and immediately put it to use. Overnight, it changed this
from a dreadful experience into something that was satisfyingly predictable.
I'll demonstrate how the system works by example. You'll see examples with standard function calls, arrow functions, methods, class
, bind
, call
, apply
, new
, super
, and even super.method()
. From studying these examples, you'll learn how to figure out how this
works in any situation you encounter.
The heart of the system is this notably compact flowchart. For now, I want you to appreciate its compactness. Don't worry about the details yet, we'll get into that in the examples. The point is that this
may be complicated, but it's not that complicated. You can get a hold of it if you're careful and use a systematic approach.
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ _____ _ _ ___ ____ ┃
┃ |_ _| | | |_ _/ ___| ┃
┃ | | | |_| || |\___ \ ┃
┃ | | | _ || | ___) | ┃
┃ |_| |_| |_|___|____/ ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
╔════════════════════════════════════════════════════════════╗
║ SPECIAL CASE ║
╚════════════════════════════════════════════════════════════╝
┌───────────┐ ┌───────────┐ ┌───────────┐ ┌───────────┐
│bind, call,│ │ new │ │ => │ │ super │
│ apply │ │ │ │ │ │ .method() │
└───────────┘ └───────────┘ └───────────┘ └───────────┘
│ │ │ │
│ │ │ │
▼ ▼ ▼ ▼
┌───────────┐ ┌───────────┐ ┌──────────────┐┌──────────────┐
│ Up to you │ │ New empty │ │Outside `this`││Outside `this`│
│ │ │ object │ │when function ││when function │
└───────────┘ └───────────┘ │ is created ││ is invoked │
└──────────────┘└──────────────┘
╔════════════════════════════════════════════════════════════╗
║ COMMON CASE ║
╚════════════════════════════════════════════════════════════╝
┌───────────┐
│ In a │ ┌───────────┐
│ function? │────No──▶│ window │
└───────────┘ └───────────┘
│
Yes
│
▼
┌───────────┐
│Invoked as │
│ method? │
└───────────┘
│
┌─────Yes────┴──────No────┐
│ │
▼ ▼
┌───────────────────┐ ┌───────────────────┐
│ │ │ window (undefined │
│ Left of dot │ │if function created│
│ │ │ on class) │
└───────────────────┘ └───────────────────┘
The flowchart is broken up into two parts. At the top, you have the "SPECIAL CASE". These cases are easily identifiable by specific keywords and language constructs.
If no special case applies, then move to the "COMMON CASE". Analyzing this
in the common case boils down to just two questions: are we "in a function?" and if so, is that function "invoked as method?".
As you can see, technically, this is very simple. The only prerequisites are (1) basic logical reasoning skills and (2) basic programming skills. But as you'll soon find, bringing everything together is easier said than done.
As you study the examples, your attention should be on my thought-process. If you pay close attention to my reasoning with each example, you should be able to figure out any novel situation on your own.
console.log(this); // this?
this
equals window
.function logThis() { console.log(this); // this? } logThis();
logThis
.logThis
is not invoked as a method.this
equals window
(not undefined
because logThis
was not created on a class).class MyClass { static method() { console.log(this); // this? } } MyClass.method();
MyClass.method
.MyClass.method()
).this
equals MyClass
(in the invocation, MyClass
is to the left of the dot).I love this example because the code seems complicated (due to class
and static
). But if you follow the flowchart, you'll cut to the heart of the matter with zero bullshit.
class MyClass { static method() { console.log(this); // this? } } const test = MyClass.method; test();
Here we have the same class from the previous example, but we're calling the function a little differently. Instead of invoking the function as a method, we're extracting it into a variable called test
, and then we're invoking test
.
MyClass.method
.test()
).this
equals undefined
because MyClass.method
was created on a class.const myObject = { method: function () { console.log(this); // this? } } function outer(input) { input(); // <-- invocation } outer(myObject.method);
To start, we need to figure out where the function is actually invoked. It's not obvious at first glance. After inspecting the outer
function, we see that myObject.method
is invoked with the expression input()
.
Now we have everything we need to know to do our analysis. Note that everything we've done so far has absolutely nothing to do with this
. We've only analyzed basic mechanics, variables, objects, and functions. So a crisp and clear fluency with the basic mechanics is an absolute prerequisite to figuring out this
.
I felt compelled to add this note because too often people think they're confused about this
or some other complicated issue, when the real problem is basic fluency with programming.
myObject.method
.// <-- invocation
comment).this
equals window
(not undefined
because myObject.method
was not created on a class).You might be wondering, wait, wasn't that a callback function in the previous example? And the answer is yes. I could have said my myObject.method
is passed into outer
as a callback function, but I didn't, because it doesn't add anything to the analysis. It's an irrelevant detail.
The key thing is determining how myObject.method
function is actually invoked. There's nothing special about callback functions. If there were, I'd have to incorporate them into the flowchart. A common problem is that many instructors introduce callback functions within the context of built-in functions.
This makes things really confusing because you can't easily see the source code for built-in functions, and so if you pass a function into a built-in function, you can't see how your function is invoked, which is key to determining this
. Basically built-in functions force you to reason about what's happening with incomplete information.
It doesn't make sense to rush into situations with built-in functions until you can confidently handle situations where you do have the full picture. That's why I'll give special treatment to built-in functions at the very end of this post. In that section, I'll also talk about a really damaging yet common misconception held by many experienced programmers when it comes to built-in functions and this
.
bind
, call
, apply
(example 1 of 1)function logThis() { console.log(this); // this? // Note that this function will be invoked 3 times. } const gordon = {name: 'Gordon'}; logThis.bind(gordon)(); logThis.call(gordon); logThis.apply(gordon);
bind
, call
, or apply
keywords.this
equals gordon
. Each time logThis
is invoked (a total of 3 times), we explicitly set the this
value to gordon
.The key idea is that bind
, call
, and apply
allow you to control the this
value of a function.
new
(example 1 of 4)function Person(name) { this.name = name; // this? } const p = new Person('Gordon');
new
.this
equals a new empty object.When you invoke a function with new
, JavaScript will set this
to a new empty object. If you want, you can optionally attach properties to the object (like name
), and then in the background, JavaScript will implicitly return this
.
new
(example 2 of 4)class Person { constructor(name) { this.name = name; // this? } } const p = new Person('Gordon');
Person
is invoked with new
.this
equals a new empty object.new
(example 3 of 4)class Person { constructor(name) { this.name = name; } thisOutside = this; // this? } const p = new Person('Gordon'); p.thisOutside === p; // true
Person
function is invoked with new
.this
equals a new empty object. This example shows that this
will be the same new empty object everywhere inside of the class body, both inside and outside of the constructor.new
(example 4 of 4)class Parent { constructor() { // P1 } // P2 } class Child extends Parent { constructor() { super(); // C1 } // C2 } const child = new Child();
Person
is invoked with new
.this
equals a new empty object. this
will be the same new empty object everywhere inside of both Child
and Parent
(since Parent
is invoked via super()
). That means that this
will be the same empty object at all points: "P1", "P2", "C1", and "C2".It's important not to confuse super()
with super.method()
, which is a completely different special case (we'll see an example of this later on).
=>
(example 1 of 1)const o = { arrowMethod: () => { console.log(this); // this? } };
=>
.this
equals outside this
when function (o.arrowMethod
) is created.To determine the this
value outside of o.arrowMethod
, we repeat the process:
this
equals window
.An important implication of arrow functions is that at creation, this
is set once and then it never changes—it's permanent. As long as you call the same arrow function, this
will always be the same.
super.method()
(example 1 of 2)class Parent { static parentMethod() { console.log(this); // this? } } class Child extends Parent { static childMethod() { super.parentMethod(); } } Child.childMethod();
parentMethod
is called as a method on super, which falls into the super.method()
case.this
equals outside this
when function (parentMethod
) is invoked.Since parentMethod
is invoked inside of childMethod
, we must determine the this
value inside of childMethod
. To answer this question, we repeat the process:
childMethod
does not fall into any special cases.childMethod
.Child.childMethod()
).this
equals left of dot, which is Child
.super.method()
(example 2 of 2)This is an especially tricky example!
class Parent { static parentMethod() { console.log(this); // this? } } class Child extends Parent { static childMethod() { super.parentMethod(); } } const test = Child.childMethod; test();
What's gonna get logged here? The two classes are exactly the same as in the previous example. Only the last two lines are different. This example would be a tricky twist for most people, but I think it should be manageable for you if you methodically use the system. So before seeing my approach, try and see if you can get the answer on your own!
super.method()
special case.this
equals outside this
when function (parentMethod
) is invoked.Since parentMethod
is invoked inside of childMethod
, we must determine the this
value inside of childMethod
. To answer this question, we repeat the process:
childMethod
does not into any special cases.childMethod
.test()
).this
equals undefined
because childMethod
was created on a class!When you pass a function into a built-in function like addEventListener
or forEach
, you can't practically inspect the built-in function's source code. And that means you can't use our system to determine what this
will be. So with that limitation, what can you do?
My favorite approach is to run experiments. I work to develop a hypothesis about what might be happening, and then carefully design experiments to test that hypothesis. I like this approach because you can generate extremely specific answers that can be hard to figure out any other way.
The most popular approach is to reach for documentation. This can be limiting though in cases where the documentation is incomplete or poorly written. Supplementing documentation with targeted experiments can be a powerful combination. This allows you to answer more specific questions and also turns reading into an active process of discovery.
The last resort is to read the underlying source code. In rare situations where you must know precisely what's happening, this may be unavoidable. Typical places to look for built-in JavaScript function are V8, Chromium, and Mozilla's SpiderMonkey.
The functions built-into JavaScript are sometimes implemented in JavaScript, but not always. For example, some are implemented in C++. So when you pass a function into a built-in function, even though the built-in function is not written in JavaScript, the built-in function will invoke your function in a way that's functionally equivalent to one of the flowchart cases.
That means you can safely operate as if all the built-in functions are written in JavaScript. So essentially, you can think of built-in functions as just normal JavaScript functions whose only shortcoming is that you can't easily see what's going on inside of them.
addEventListener
In this example, I want to run targeted experiments to understand how addEventListener
works.
// Create a button, b. const b = document.createElement('button'); // Add b to document.body. document.body.appendChild(b); // Add click handler. b.addEventListener('click', function myCallback() { console.log(this); // this? });
Each time we click the button b
, the this
value inside of our myCallback
will get logged to the console.
The problem here is that some code kicked off by addEventListener
will invoke myCallback
, but we can't see how, because addEventListener
is a built-in function.
So the next best thing is to see what it does and then try to reason about what might be happening. If you run the code and click b
, we can see that b
itself gets logged to the console. From here, we can use our flowchart to reverse-engineer what might be happening.
First, we consider whether we have a special case. We can't rule out bind
, call
, or apply
. However, we can rule out new
and =>
because we're not in a constructor or arrow function. We can also rule out super.method()
because we're not in a class.
Now let's consider whether a common case situation might apply. We're in a function, but we can't directly observe if it was invoked as a method. However, we can rule out the possibility that myCallback
was not invoked as a method, because if it were, this
would have to be either window
or undefined
.
Finally, myCallback
could have been invoked in the common case, as a method. But it's very unlikely because myCallback
would have to be a property of b
. This would be fine if you have just one event listener, but an element can have many, so it could get really, really messy. So while theoretically possible, this scenario is extremely unlikely.
So that leaves just one plausible option, which is that addEventListener
is explicitly setting the this
value inside of myCallback
to b
using bind
, call
, or apply
.
addEventListener
Many people use the mistake of assuming that this
inside of the myCallback
must be b
, because b
is to the left-of-the-dot in b.addEventListener(function myCallback() {})
.
This reasoning is wrong! It is true, assuming no special cases, that this
inside of addEventListener
will be equal to b
, because addEventListener
is invoked as a method, but that says nothing about what this
will be inside of myCallback
, which is the function in question.
In order to think about this
in myCallback
, you must see how myCallback
is invoked, and you simply cannot tell by looking at the code here. So be careful, it's really easy to make these subtle reasoning mistakes. In the course of doing research for this post, I saw several popular tutorial authors make this exact error!
It's disheartening to see so many "teachers" spreading this flawed thinking. Some might argue that it doesn't matter, because you can make this error and come to the right conclusion. But this argument essentially defends the broken clock that is right twice a day.
forEach
[1].forEach(function testFunction() { console.log(this); });
In this example, I want to figure out how forEach
works just by running experiments, without reading any documentation. forEach
will run testFunction
, but we can't see how, because forEach
is a built-in function.
If we run the program, we see that window
gets logged to the console. From here, we can use our flowchart to reverse-engineer what might be happening.
First, we consider whether we have a special case. We can't rule out bind
, call
, apply
(Possibility A). However, we can rule out new
and =>
because we're not in a constructor or arrow function. We can also rule out super.method()
because we're not in a class.
Now let's examine the common cases. We're in a function, so the next question in the flowchart is whether the function was invoked as a method.
If testFunction
were not invoked as a method (Possibility B), this
would be window (since the testFunction
was not created on a class), which lines up with what's actually logged to the console.
If testFunction
were invoked as a method (Possibility C), testFunction
would have to temporarily be saved as a method on window
at the time of invocation. I say temporarily, because if we inspect window
after the program runs, we can see that no such method exists.
So that leaves us with 3 distinct possibilities that we must narrow down:
forEach
invokes testFunction
with bind
, call
, or apply
and explicitly sets this
to window
.forEach
invokes testFunction
in the common case, not as a method.forEach
is invoking testFunction
as a method on window
.After pondering this for a while with the flowchart in front of me, I wondered to myself, "what if we gave forEach
a function that was initially created on a class?" Would that give us any useful information?
Stepping back, this is an application of a powerful problem-solving technique where you hold fixed a portion of the problem (in this case forEach
) and vary the other elements (the function argument).
Here is the modified experiment:
class MyClass { static method() { console.log(this); // ? } } [1].forEach(MyClass.method);
From here, I thought about what would happen in each of the three possibilities (the "implications" I mentioned earlier):
this
will still be window
.this
will still be undefined
since method
was created on a class.this
will still be window
.So if we see window
logged to the console, then it would mean that we either have Possibility A or Possibility C, and we'd have to do more work to narrow it down.
However, if we get undefined
, then it would mean that it must be Possibility B. If we run the program, we see that undefined
is logged to the console. That means we can know with certainty, that forEach
invokes its argument in the common case, not as a method.
Armed with just a simple flowchart and basic problem-solving skills, you can answer extremely sophisticated questions that are impractical to figure out any other way.
Each time you see a piece of code, I want you to ask yourself, "what is this
equal to"?" If you do that enough times, and introspect each time you're wrong, eventually you should be able to get it right every single time. If you do make a mistake, that's okay, but only if you take that as an opportunity to figure out what went wrong so that you can avoid making that mistake again.
That's the whole point of this type of deliberate practice. My main hope from writing this post has nothing to do with this
keyword. I really couldn't care less. This kind of language specific thing to me at least, is the least interesting, least important, most boring part of programming.
My real hope is that you got a sense of how powerful it can be to approach problems in a more structured, more systematic way. All of my teaching is built on the similar idea that how you think is more important than what you know.
Anyone can memorize and regurgitate information, and computers are better than us at doing that anyway. Instead, I want you to get better at knowing what to do when you don't know the answer.