It is said that "Context is Everything."
Execution Context (or just "context") is a critically important concept in JavaScript, but it often doesn't get the attention it deserves in courses and training. This results in context becoming a mysterious JS voodoo, which creates a strong aversion to the very useful this
keyword. When new coders are forced to use it, it's often in a 'try it and pray' fashion where they use it, then see if/how it breaks the code, then try something else until they finally get it to work.
That is not good - let's fix it!
In this post I'm going to try to:
- Put three clear rules around identifying
this
and demonstrate with some very simple examples. - Then I'll demonstrate a very common (and confusing) issue and explain how to solve for it.
What is Execution Context in JavaScript
First rule: Execution context comes into play whenever you are using a function.
Every single function will have some execution context when it runs, whether you want it to or not. Therefore, you should always stop and think about what the context of your function is - this will go far in helping you avoid nasty surprises.
Second rule: "Execution Context" refers to what is performing the function.
Execution context gets set at the function's call-time, and can be set either explicitly or implicitly. This means that even if you think you didn't set any context, the function will implicitly create it's own.
If there is no subject, it will get auto-set to the global object (this point is important, more on it later). A good heuristic I've found is to read the function like it is a sentence - if I can't do that because nothing seems to be performing the action, it's most likely the global object.
Third rule: The keyword this
simply returns whatever the current execution context is while the function is being run.
You can think of using this
in a function as saying "do the action to yourself."
If the context is the global object, using this
tends to result in undefined
bugs, that nasty surprise I mentioned in the first rule.
Putting in Into Practice - Examples
So how do we use these three rules to understand what is the execution context, and what this
would refer to?
Example 1
Let's start with this expression:
const fruits = ["apple", "blueberry", "cherry"];
- Rule 1 says if it's a function it has execution context. This is not a function; therefore we can stop concerning ourselves with it. Execution context is global object by default, but we don't really care.
Example 2
Now let's try with this expression:
function barkAtDoor() {
console.log("Bark, Bark");
};
barkAtDoor();
- Since it is a function, Rule 1 says that there must be some execution context. So what is it?
- Rule 2 says we can find out by asking "what is performing this action?" If we can't answer that, it's most likely the global object. Reading the function as a sentence: "__ says "Bark, Bark." we can see that there is no What, so the context is still the global object.
- Rule 3 says that
this
returns the current context, so if we were to use it here, it would refer to the global object, likely resulting in a bug.
Example 3
Finally, what about this one?:
const pup = "Fido";
function barkAtDoor() {
console.log("Bark, Bark");
};
barkAtDoor.call(pup);
- Rule 1: It is a function
- Rule 2: pup says "Bark, Bark". Look at that, we have a subject, therefore, the execution context in this scenario is pup.
- Rule 3:
this
would refer to the pup, which is "Fido" in this case, so if we were to throw athis
into the console.log, it would print "Fido". This is an example of explicitly assigning execution context.
A Shortcut - Function? Or Method?
Now that we know the hard way to do it, here's a shortcut. As with most shortcuts it is not a 100% effective solution, so keep that in mind.
A "method" is a special type of function. Where a function can stand on its own, a method is directly associated with some object. This is important because a method takes on the context of the object it is called on. In other words the context is implicitly assigned (Rule #2). Luckily in beginner JavaScript, we call methods using dot notation: subject.method()
so it's easy to recognize the subject. Here's an example of the two:
const article = "Dan's newest post"
// function
function readArticle(article) {
console.log(`Wow, ${article} was a great article!`);
}
readArticle(article); // Wow, Dan's newest post was a great article!
// method
let reader = {
name: "Joe",
readArticle(article) {
console.log(`Wow, ${article} was a great article!`);
}
}
reader.readArticle(article) // Wow, Dan's newest post was a great article!
In that code snippet, calling the readArticle function and method would return the same result, so on the surface they seem like the same thing. BUT if you think carefully and try to read them as a sentence you'll get:
- function: __ says "Wow, Dan's newest post was a great article!"
versus
- method: The reader says "Wow, Dan's newest post was a great article!"
Now, let's see those same examples again, but this time adding this
to see what happens.
const article = "Dan's newest post"
// function
function readArticle(article) {
console.log(`${this} commented: Wow, ${article} was a great article!`);
}
readArticle(article); // [Object Window] commented: Wow, Dan's newest post was a great article!
// note: any this attribute would produce undefined
// method
let reader = {
name: "Joe",
readArticle(article) {
console.log(`${this.name} commented: Wow, ${article} was a great article!`);
}
}
reader.readArticle(article) // Joe commented: Wow, Dan's newest post was a great article!
So if you know that the function being called is a method, you can typically look to the left of the dot to quickly identify what the context is.
The Gotcha - Callback and Arrow Functions
Consider this:
const reader = {
name: "John",
booksRead: ["Catcher In The Rye", "Dune"],
reviewBooks() {
this.booksRead.forEach(function(book) {
console.log(this.name + " liked " + book);
});
}
}
reader.reviewBooks();
// undefined liked Catcher In The Rye
// undefined liked Dune
What gives? reviewBooks is a method, so whatever is to the left of the dot should be the execution context right? Well...it is...for the actual reviewBooks method. BUT, Rule 1 says that all functions create an execution context. The issue then, is that the callback function inside of the method is creating its own context. Let's analyze with our rules:
- Rule 1: it is a function, so it has created a context.
- Rule 2: __ says this liked book. We can't fill in that blank since the callback is not a method of reader and was not called with reader explicitly being assigned. So our context is actually the global object.
- Rule 3: this will return the global object, resulting in the undefined error since the global object won't have a name attribute.
Well that's confusing, and it's one of the downfalls of the newer developer. But again, if you consciously think about the scope for EVERY function you write, you'd be able to catch this. This doesn't only happen for callbacks either, nested functions can cause the same issue.
So what can we do about it?
As with everything in coding, there are multiple ways to handle this, but the best and most common way to handle it is to convert that callback into an arrow function (in ES6 or higher). Arrow functions are special because they are the only exception to Rule #1. They do not create their own context and instead they simply "carry over" their parent function's context. So this minor change will give you the predictable results you want.
const reader = {
name: "John",
booksRead: ["Catcher In The Rye", "Dune"],
reviewBooks() {
this.booksRead.forEach(book => {
console.log(this.name + " liked " + book);
});
}
}
reader.reviewBooks();
// John liked Catcher In The Rye
// John liked Dune
Hope this helps to clarify Execution Context and this
. It certainly takes some getting used to, but the first step is to start thinking in terms of these three rules EVERY TIME you write a function.
Top comments (0)