Execution context, implicit and explicit binding, arrow functions, and of course, this.
To me, a competent understanding of the keyword this
in JavaScript feels just about as hard to master as the final boss in a D&D campaign.
I am not prepared.
My instinct therefore tells me I should write an article on the subject so that I can attempt to move this
from my passive vocabulary to my active vocabulary.
Join me on this quest, and let's level up together.
Levels 1–4: Local Heroes
Defining execution context, this
, and new
.
Before we look at this
, it will be helpful to define execution context. MDN describes execution context in this way:
When a fragment of JavaScript code runs, it runs inside an execution context. There are three types of code that create a new execution context:
The global context is the execution context created to run the main body of your code; that is, any code that exists outside of a JavaScript function.
Each function is run within its own execution context. This is frequently referred to as a "local context."
Using the ill-advisedeval()
function also creates a new execution context.Each context is, in essence, a level of scope within your code. As one of these code segments begins execution, a new context is constructed in which to run it; that context is then destroyed when the code exits.
Put simply, an execution context is a created space that directly surrounds a block of running code.
A global context is created at the start of a program. Each time a function is invoked, a local execution context gets created and is added to the execution context stack (the call stack).
I see other articles refer to execution context as the "environment" surrounding your block of running code, which makes sense, because it's more than just a location!
An execution context also has custom properties.
These properties are a little different between global and local context, and between different types of local context (functions vs arrow functions vs classes, etc).
Something that every execution context has however, is a property known as this
.
So what is this
?
Drum roll…
this
is a reference to an object.
But what object? Well, that depends. Time to run through some examples to start figuring it out.
In our first simple example, we log the value of this
in the global execution context, outside of any function:
console.log(this); // window
In the browser, we see that this
equals the window
object.
MDN confirms the behavior in this definition:
In the global execution context (outside of any function),
this
refers to the global object whether in strict mode or not.
SIDE NOTE: In the browser the global object is window
. In Node.js, the global object is actually an object called global
. There are some slight behavioral differences though. In Node.js, the example above would actually log the current module.exports
object, not the global
object. 🤷 Let's avoid that rabbit hole for now.
In this next example, we log this
inside a function:
function logThisInAFunction() {
console.log(this);
};
logThisInAFunction(); // window
At first, you might think that this
will evaluate to something other than window
, seeing as we created a new local execution context upon invoking the function, which the code inside our function runs within.
But as a default (when not using strict mode, another rabbit hole 🤷), unless explicitly set (which we will get to), this
still references window
.
MDN confirms this default behavior:
Inside a function, the value of
this
depends on how the function is called.Since the following code is not in strict mode, and because the value of
this
is not set by the call,this
will default to the global object, which iswindow
in a browser.
SIDE NOTE: There is a weird difference in behavior between the browser and Node.js in this second example. In the browser, this
equals window
, but in Node.js, this
equals global
this time, instead of the module.exports
object. This is due to the difference in how Node.js handles the default value of this
between global and local contexts.
The next example expands on this
having a different value based on how the function is called:
function logThisInAFunction() {
console.log(this);
};
const myImportantObject = {
logThisInMyObject: logThisInAFunction
};
logThisInAFunction(); // window
myImportantObject.logThisInMyObject(); // myImportantObject
When we call logThisInAFunction()
and myImportantObject.logThisInMyObject()
, the same function is ultimately being invoked. However, the value of this
is not the same for the two invocations. MDN again eloquently describes:
When a function is called as a method of an object, its
this
is set to the object the method is called on.
Put simply, if you call a function as a method of an object, that object is going to become the value for this
instead of the global object. In this way, we are implicitly binding the value of this
to an object of our choosing.
The next example looks at how this
behaves for classes:
class MyAwesomeClass {
constructor() {
console.log(this);
};
};
const awesomeClassInstance = new MyAwesomeClass(); // MyAwesomeClass {}
When creating an instance of a class using the new
keyword, a new empty object is created and set as the value of this
. Any properties added to this
in the constructor will be properties on that object.
The behavior of
this
in classes and functions is similar, since classes are functions under the hood. But there are some differences and caveats.Within a class constructor,
this
is a regular object. All non-static methods within the class are added to the prototype ofthis
.
Relatively straight forward.
Before we level up…
Hopefully this
is starting to sink in. Having already talked about implicit binding, let us explore the next level: explicit binding.
Levels 5–10: Heroes of the Realm
Defining call
, apply
, and bind
.
Earlier we saw how we can take a function, add it as a method to an object, and when we invoke that method, that object implicitly becomes our new value for this
.
Now we want to learn how to explicitly set the value of this
when invoking a function. We can do that with a few different methods: call
, apply
, and bind
.
First, an example using call
:
const wizard = {
class: 'Wizard',
favoriteSpell: 'fireball'
};
const warlock = {
class: 'Warlock',
favoriteSpell: 'eldrich blast'
};
function useFavoriteSpell(name) {
console.log(`${name} the ${this.class} used ${this.favoriteSpell}!`);
};
useFavoriteSpell('Bobby'); // Bobby the undefined used undefined!
useFavoriteSpell.call(wizard, 'Bradston'); // Bradston the Wizard used fireball!
useFavoriteSpell.call(warlock, 'Matt'); // Matt the Warlock used eldrich blast!
The first time we invoke useFavoriteSpell
, we have undefined values. this
has defaulted to referencing the window
object, and the properties class
and favoriteSpell
do not exist on window.
The next two times we invoke useFavoriteSpell
, we are using call
to assign the value of this
to the object of our choosing, and also invoking the function.
That is what call
does! The first argument of call
is the object you want this
to equal, and the subsequent comma separated arguments are the function arguments.
You can read more about call
here.
The next method we will look at is apply
. No additional code example is necessary to understand it, in my opinion. The main difference between call
and apply
is this:
call
accepts function arguments one by one in a comma separated list.apply
instead accepts all function arguments as one array.
Outside of some other small differences, they do the same thing. You can read further about apply
here.
Finally, we have bind
:
const wizard = {
class: 'Wizard',
favoriteSpell: 'fireball'
};
const warlock = {
class: 'Warlock',
favoriteSpell: 'eldrich blast'
};
function useFavoriteSpell(name) {
console.log(`${name} the ${this.class} used ${this.favoriteSpell}!`);
};
useFavoriteSpell('Bobby'); // Bobby the undefined used undefined!
const useFavoriteWizardSpell = useFavoriteSpell.bind(wizard);
useFavoriteWizardSpell('Bradston'); // Bradston the Wizard used fireball!
const useFavoriteWarlockSpell = useFavoriteSpell.bind(warlock);
useFavoriteWarlockSpell('Matt'); // Matt the Warlock used eldrich blast!
Instead of setting the value of this
and also invoking the function, calling bind
on our function just returns a new function, whose this
value is now equal to the object we passed as the argument of bind
.
We can then call our new function whenever we want, and the value of this
has already been set.
Read more on bind
here.
Before we level up…
So now we know about execution context, this
, and how to set a value for this
both implicitly, and explicitly with call
, bind
, and apply
.
What else is there? To be honest, there is a lot. But let's focus on one thing at a time, and move on to the relationship between this
and arrow functions.
Levels 11–16: Masters of the Realm
How arrow functions affect this
.
Earlier, I said that every execution context has a property known as this
. The behavior with arrow functions is a little different, however.
While the local execution context created by invoking an arrow function does still have a value for this
, it does not define it for itself. It instead will retain the value of this
that was set by the next outer execution context from where the function was invoked.
For example:
const logThisInAnArrowFunction = () => {
console.log(this);
};
const myImportantObject = {
logThisInMyObject: logThisInAnArrowFunction
};
logThisInAnArrowFunction(); // window
myImportantObject.logThisInMyObject(); // window
We see when we call myImportantObject.logThisInMyObject()
that even though a new local execution context was created, this
is going to get its value from the next outer execution context. In this case, it is the global execution context. Therefore, this
remains a reference to the window
object.
Here is another example that hopefully will drive it home:
const myImportantObject = {
exampleOne: function() {
const logThis = function() {
console.log(this);
};
logThis();
},
exampleTwo: function() {
const logThis = () => {
console.log(this);
};
logThis();
}
};
myImportantObject.exampleOne() // window
myImportantObject.exampleTwo() // myImportantObject
In this example we have two methods we call from myImportantObject
: exampleOne()
and exampleTwo()
.
When we call myImportantObject.exampleOne()
, we are invoking that function as a method of an object, and therefore, this
equals myImportantObject
in that local context.
However, inside that function, we define another function and then execute it.
As previously mentioned, unless explicitly set, and unless the function is being invoked as a method of an object, this
references the global object, window
. Therefore, we see window
logged.
When we call myImportantObject.exampleTwo()
however, something different happens. The first part is the same: this
equals myImportantObject
inside exampleTwo
. Next, we define an arrow function and then execute it. The difference is, the arrow function does not define its own value for this
! It instead retains the value of this
from the next outer execution context, which in this example, was the local context created by myImportantObject.exampleTwo()
, where this equalled myImportantObject
. So that is what we see logged.
Before we level up…
Arrows functions differ from regular functions in plenty of other ways. If you want to read more, check out MDN's page on the topic.
If you have made it to this point in the article, it should hopefully mean that things are starting to sink in. Take a moment to relish in your achievement.
There's only one section left. Let's dive in.
Levels 17–20: Masters of the World
Callbacks.
Before I started writing this article, I thought the last section was going to be the most difficult to tackle. In the past, examples of how this
got its value in callback functions confused me.
With the context I now have, it no longer feels like callback functions add any more complexity to how this
works. We just need to consider what we already know about execution context, and how we invoke a function.
I guess the final boss is starting to look a little more beatable 😁.
Here is an example of using this
within callback functions. See if you can guess what the values logged for each will be:
function higherOrderFunction(callback) {
const myImportantObject = {
callMyCallback: callback
};
myImportantObject.callMyCallback();
};
function callbackFunction() {
console.log(this);
};
const callbackArrowFunction = () => {
console.log(this);
};
higherOrderFunction(callbackFunction);
higherOrderFunction(callbackArrowFunction);
Starting with our first function call (where the argument is callbackFunction
):
higherOrderFunction
is invoked, and creates a local execution context. Because it is not implicitly or explicitly set,this
defaults towindow
.Our callback function is invoked inside
higherOrderFunction
viamyImportantObject.callMyCallback()
. Because our callback is not an arrow function, the created local execution context defines its ownthis
. And, because that function is being invoked as a method ofmyImportantObject
,this
equalsmyImportantObject
.
Next, let's look at the second function call (where our argument is callbackArrowFunction
):
higherOrderFunction
is invoked just as before. Same outcome:this
equalswindow
in that local context.Our callback is once again invoked inside
higherOrderFunction
. This time, because our callback is an arrow function, it does not define its ownthis
. So even though it is being invoked as a method ofmyImportantObject
,this
retains the value of the next outer execution context, which waswindow
.
The moral of the story is, callback functions add complexity only in the sense that you have to consider how (and also where for arrow functions) the callback is being invoked.
But we were considering that already!
The difference is that callback functions are passed as arguments to other functions, and so where and how they get invoked is a little different.
Parting Thoughts
Hopefully I didn't lose you at some point 😅.
As a developer, I am always trying to continue my education - and I recognize that there is still so much I do not know.
this
is a big subject, and I am sure that there are aspects of the concept that I missed, or examples and explanations that could be improved. Let's help each other by trying to figure out what those were! Feel free to leave a comment, and let me know if this article helped you, and/or what could have been improved.
Thanks for reading! 😄 Check out some of my earlier articles at quickwinswithcode.com.
Top comments (0)