I spent the first few years of my career regarding this
as yet another aspect of my primarily hunch-driven approach to web development—which is to say, sometimes it felt like this
might be what I wanted, but if so, it was for reasons I couldn’t possibly begin to understand.
I was occasionally right—to my very slim credit—but not because I knew why or anything. Now, after years of muddling through on vibes, trial and error, and squinting at Stack Overflow for as long as impending deadlines would allow, I think I’ve gotten a pretty good handle on this
. I mean, give or take an emotional support console.log
every so often; I’m only human.
For me, the trouble was always that this
is contextual, but that context isn’t meaningful to us developers so much as it’s meaningful to JavaScript. You and I are used to telling JavaScript what things are: this is the identifier we typed, where we typed it, scoped to the pair of brackets we typed around it, and it has the value we assigned to it.
That’s not the case with this
, which is—brace for it—a keyword that refers to the object bound to the function where this
is invoked at the time when that function is called.
That is an absolute nightmare of a sentence, I know, but bear with me. It breaks down to two important concepts: this
references the object bound to a function, and the object referenced by this
is determined at the time a function is called. Once you’ve got a handle on those two aspects, this
is— well, still a little weird, but the knowable kind of weird.
At a high level, “the object bound to a function” makes sense when you frame it in terms of the essential nature of JavaScript: objects with methods referencing objects with methods, all the way down. Almost any code we execute anywhere in a script relates to an object in one way or another, either explicitly or implicitly. I wouldn’t say that’s an uncomplicated topic, strictly speaking, but we’ll get to that in a bit.
We can’t understand the what without first understanding the when, and for my money, “at the time a function is called” is what makes this
particularly tricky to get a handle on. What determines the value of this
isn’t the writing, but the calling of a function—meaning that the value of this
inside a function could be different every time that function is invoked. In order to really make sense of this
, we have to think more like JavaScript executing a function than a person at a keyboard writing a function.
Execution Contextspermalink
To better understand how JavaScript “thinks,” we need to better understand JavaScript’s execution model and the nature of the call stack—the “first in, last out” data structure that a JavaScript interpreter uses to execute code.
Again, you and I are used to thinking about the JavaScript we write in terms of the JavaScript we’re writing. We’re calling the shots: when, where, and how we declare a variable and assign it a value communicates its relevancy to JavaScript.
- Code language
- js
function theFunction() { const theVariable = true; };
When a JavaScript engine encounters this code, it creates a lexical environment for theFunction
—a data structure that represents the code as written.
All the context needed to execute that function—our variable, in this example—is added to the environment record for the function. If you’ve ever wondered how JavaScript can be aware of variables prior to declaring them (but gives you a hard time about it), those environmental records are why:
- Code language
- js
function theFunction() { console.log( theVariable ); let theVariable; } theFunction(); // result: Uncaught ReferenceError: can't access lexical declaration 'theVariable' before initialization
The environmental record created for this function’s execution context contains a theVariable
property, because it’s right there in the lexical environment for the function. JavaScript is aware of our variable at the time it executes the function and encounters that console.log
, but we’re doing something pretty weird with it, so JavaScript throws an error to save us from ourselves. That environmental-record-based “awareness” is how variable hoisting works, but that is a topic for another time.
After making lexical sense of the function and creating the environmental record, the JavaScript engine creates an execution context (sometimes called a “frame”) for our function. That execution context includes the function and everything required to execute it.
The Call Stackpermalink
You can think of the call stack as a sort of playlist made up of these function execution contexts: it determines what code is executed and when. This probably goes without saying, but there’s a lot going on in and around the call stack—if you’re interested in learning the gritty details about how it works and how it impacts the code we write, well, do I ever have a course for you.
For now, and purposes of understanding this
, here’s the short and synchronous version of how the call stack works.
Given the following script:
- Code language
- js
const theValue = 4; function checkTheValue( theFunctionVariable ) { const successMsg = "High enough."; const failureMsg = "Too low."; if( theFunctionVariable > 2 ) { consoleLogger( successMsg ); } else { consoleLogger( failureMsg ); } }; function consoleLogger( msg ) { console.clear(); console.log( msg ); }; checkTheValue( theValue );
When a script is executed, the JavaScript interpreter creates a “global execution context” and pushes it to the call stack. Any statements inside that global context are then executed one at a time, from top to bottom.
As soon as the interpreter encounters that checkTheValue
function call within the global execution context, it creates a function execution context for that call at the top of the stack. The global execution context is put on hold, and the newly-added function execution context is immediately executed.
That function execution context contains a call to consoleLogger
, so a function execution context for that call is added to the top of the stack and—-you guessed it—executed immediately.
Once consoleLogger
has concluded, it gets removed from the stack, and JavaScript continues executing the checkTheValue
function that called it.
Our checkTheValue
function concludes, is popped off the stack, and the global execution context continues on.
Well, it would continue on if it had anything else to do. Since there aren’t any other statements to execute, that gets removed as well—“first in, last out.” At that point there’s nothing left in the call stack, so our script has concluded.
When is the value of this
determined?permalink
A number of things happen when a JavaScript interpreter creates an execution context, one of which is setting a value for the keyword this
within the scope of that execution context. Except in one specific case, this doesn’t depend on the lexical environment—we don’t dictate the value for this
within the scope of the execution context for the function where we reference it. The value referenced by this
within the execution context depends on how that function was called: whether a function is called as a standalone function, a method, or a constructor can change that value.
For my money, this is the heart of the confusion around this
. When we write a function that contains const theVariable = true
, it doesn’t matter what route a JavaScript engine takes to reach that statement: the lexical environment contains a statement initializing theVariable
, so the immutable fact that theVariable
exists is marked down in the environmental record. If and when that statement is reached in the execution context, the value of theVariable
is true
, full stop:
- Code language
- js
const theObject = { theMethod() { const theVariable = true; console.log( theVariable ); } }; theObject.theMethod(); // result: true
You couldn’t possibly be faulted for assuming this
works the same way, despite the fact that JavaScript is doing the setting instead of us (don’t worry about the specifics in this example just yet):
- Code language
- js
const theObject = { theMethod() { console.log( this ); } }; theObject.theMethod(); // result: Object { theMethod: theMethod() }
We’ll talk about the nature of “the object bound to the function” in the second part of this article series for sure, but here we see that this
logged inside theMethod()
results in the object containing that method (theObject
). Even for all my disclaimers, this snippet of code probably still reads a lot like this
in the method refers to the object because, well, look at it—the method is right there in that object. However, if we invoke the same method in a slightly different way:
- Code language
- js
const theObject = { theMethod() { console.log( this ); } }; const theFunctionIdentifier = theObject.theMethod; theObject.theMethod(); // result: Object { theMethod: theMethod() } theFunctionIdentifier(); // result: Window {...}
There’s that contextual weirdness. The same method and the same object—but if we invoke the method a little differently, the value of this
changes:
- Code language
- js
const theObject = { theMethod() { console.log( this === theObject ); } }; const theFunctionIdentifier = theObject.theMethod; theObject.theMethod(); // result: true theFunctionIdentifier(); // result: false
“At the time the function is called.”
Not the most intuitive thing in the world, granted, but at least the when for setting the value of this
makes a kind of deep-down-in-the-JavaScript-machinery sense.
Without understanding the “when” of this
, we wouldn’t be able to make sense of the “what”—specifically, what objects this
could reference, depending on how a function is called. In the second part of this series of articles that’s exactly what we’re going to do, and there’s good news: there are only a handful of different cases that determine the object referenced by this
.
Catch you in the next one.
With thanks to Jake Archibald for checking all of the specifics were in order.