DEV Community

Cover image for Essential "this" concepts in JavaScript
Robert Tate for Developers @ Asurion

Posted on

Essential "this" concepts in JavaScript

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-advised eval() 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 is window 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.

As MDN puts it:

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 of this.

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!
Enter fullscreen mode Exit fullscreen mode

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!
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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 to window.

  • Our callback function is invoked inside higherOrderFunction via myImportantObject.callMyCallback(). Because our callback is not an arrow function, the created local execution context defines its own this. And, because that function is being invoked as a method of myImportantObject, this equals myImportantObject.

Next, let's look at the second function call (where our argument is callbackArrowFunction):

  • higherOrderFunction is invoked just as before. Same outcome: this equals window 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 own this. So even though it is being invoked as a method of myImportantObject, this retains the value of the next outer execution context, which was window.

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.

Resources

Discussion (0)