You know those concepts that you learn over and over again, yet they seem to vanish from your memory no matter how many notebooks you fill with detailed notes?
The this
keyword in Javascript was one of those concepts for me, until I finally took an afternoon to understand the inner-workings of the code rather than hopelessly trying to memorize the 4 types of "bindings" (default, implicit, explicit, and "new") that influence the value of this
.
Grab a coffee, put your focus cap on, and stay with me for the next 15 minutes and you too will finally understand what the this
keyword is all about.
Compile Time vs. Run Time
Writing code does not require a deep understanding of compilers and runtimes, and if you learned to code in the 21st century, there probably wasn't someone force-feeding you operating system theory.
But to understand the this
keyword in Javascript, we need at least a basic understanding of the difference between compile time and run time.
All languages are slightly different in how they compile and execute code, but Javascript is a bit unique in this area. Since Javascript runs in the browser, it needs to be compiled and executed all at once; and fast! As opposed to a language like C where we must first compile our code and then run it, this "all in one" magic makes it seem as if the Javascript Engine (usually a browser like Chrome) is one entity that does everything.
But hold on, I'm not sure I know what "compilation" is?
You've probably read the word a thousand times, but what does it actually mean?
Compilation can be understood in two contexts:
- As a process
- As a point in time
Let's throw some code on the screen to understand this better.
function simpleFunction() {
console.log("I am a useless function");
}
simpleFunction();
Defined as a process, compilation of this code is the process of getting it from what you see on the screen now to the 1s and 0s (binary code) that a computer can execute.
The specifics of how this happens is defined by the compiler itself, but it might go something like this:
- Hmmm... I see the word "function" first. Is this part of the Javascript language syntax? Yep! Okay, let's move on.
- I just saw the keyword "function", so the next word "simpleFunction" must be the name of it. Does this name meet my standards? It doesn't have any odd characters or spaces, so yep! Next!
- I see a "{", which is what I might expect after seeing "function" and "simpleFunction". All good here.
- What is this thing called "console"? Not really sure, but it must be an object. Let's see if a "." comes next.
- Ahh, there it is! The "." means some sort of method is coming.
- Yep, "log" is the method being called on "console". Don't know if it actually exists, but that's not my job! Moving onward!
- I see a "(" character. I expect that because I just saw a method called "log" right before this. We must be defining parameters to this method now.
- I see a string "I am a useless function". Hmmm, don't know what the point of that is, but it's valid string syntax, so I'll accept it.
- And here is the closing parenthesis ")" followed by a ";". This method is done and valid!
- And now, I see a "}". This means that my function definition is now complete!
- I see "simpleFunction" again with "()". This is a valid way to call the function that was declared earlier.
- And that's it! No more tokens to parse through. This program is compiled.
After reading through this imaginary conversation with the compiler, you can see that the compiler's job is to go through a program, look at the symbols (also called "tokens"), and determine whether they make sense according to the language specifications. If the compiler saw the code below, it would get mad and throw an error without compiling the code into 1s and 0s:
variable myvariable = 1;
Here's how that conversation would go:
- I see a word "variable". There is no "const", "let", or "var" at the beginning of this line, so this must be an implicitly declared variable. I don't love it, but technically it's valid!
- Whoaaaaaa whoaa whoaa hold on here. I was fine with the previous line, but now I'm seeing "myvariable". This is not valid Javascript syntax. I'm throwing a SyntaxError!
As you can glean from the examples above, when we look at compilation in the context of a process, it is all about reading code, validating it, and transforming it into something a computer can then execute.
But many experienced developers will talk about this thing called "compile time", which is viewing compilation in the context of a point in time.
This is much harder to understand because as you saw, compilation is more of a process than a point in time.
When you hear "compile time", this is really referring to that moment right before you hit compile, or in our case with Javascript, run the program.
So really, "compile time" is another way of saying "what our code looks like before the compiler transforms it".
Run Time
The Compiler is great at making sure that your code has the correct syntactical structure, but it doesn't really check to make sure the code works.
invalidFunction();
If you run this in a Javascript console, you'll get ReferenceError
because the compiler compiled the code down, but when the Javascript Engine tried to run it, it couldn't find a declaration invalidFunction
anywhere.
So the run time is when the program is being executed, which includes things like the call stack, memory locations, etc.
"Run Time" vs. "Runtime"
I think where things get confusing is the lack of distinction online between the phrase "run time" and word "runtime".
We know that "run time" is once the program has started executing, but we haven't yet asked where it is executing.
I can open up Google Chrome and in the Developer Tools, go to the console. Once I am there, I can write and execute Javascript code.
I can also open up the terminal on my computer, type node
, and I will enter the NodeJS console where I can write and execute code.
I've written the same code in two different runtimes.
But why do we need different runtimes?
Because a Windows Computer is different than a Mac computer which is different than a Browser. Specifically, their hardware components and thus their assembly languages that high level code like Javascript needs to be compiled into are different!
When Javascript is compiled down into the 1s and 0s that the computer can run, it needs to keep in mind the runtime environment that it is in. If it doesn't, it may end up with Windows low-level system calls happening on a Mac, which would obviously not work!
Coming back to the "this" keyword
So we chatted about how compilation and runtimes mean different things when viewed in different contexts. Compilation as a process refers to the transformation of code from what the developer writes to what the computer reads. What the computer reads happens during the process of runtime and is different depending on the "runtime environment".
But to understand the this
keyword in Javascript, we have to think about run time and compile time from the context of a point in time.
Static (lexical) vs. Dynamic Scope
The reason we must look at compile time and run time from the context of a point in time is because the values of your variables and functions are entirely dependent on whether they are being defined at run time or compile time!
Understanding static (lexical) vs. dynamic scope is the last item you must understand before the this
keyword starts making sense!
What is "Scope"?
If you're reading this still, you probably have an idea what scope is already. Take a look at the following code:
let a = 1;
function printA() {
a = 2;
console.log(a);
}
printA(); // 2
console.log(a); // 1
When we call printA()
, it will first look for the value of a
within the scope of the printA
function, and since that value exists, it will print that value.
Since the console.log
statement does not have access to the scope of printA
, it has to look in the global scope, which is the only scope it has access to.
In other words, the Javascript Engine will look for the variable in the current scope, and if it cannot find it, it will look up one scope. If it gets to the global
scope and still cannot find the variable, then a ReferenceError
will be thrown because that variable does not exist.
Here is a contrived example of this process:
let globalVariable = 2;
function outer() {
middle();
function middle() {
inner();
function inner() {
console.log(globalVariable);
}
}
}
outer(); // 2
inner(); // ReferenceError: inner is not defined
When we call the outer
function, this function calls the middle function which calls the inner function. When the inner function is called, it first looks for the value of globalVariable
in its own scope. It doesn't find it, so it then looks in the scope of middle
. It again doesn't find it, so it looks in the scope of outer
. It doesn't find it, so it finally looks in the global scope. It finds it there and prints a value of 2.
On the other hand, when we call the inner
function from the global scope, a ReferenceError
is thrown!
This is because scopes in Javascript (and pretty much any language) work in only one way. In this case, the scope of inner
is "encapsulated" and therefore, the global scope does not even know that the inner()
function exists.
Makes sense, but why?
You probably didn't realize it, but likely, all the programming languages that you have used implement static, or "lexical" scope--Javascript included. What I just explained are static scoping rules.
But there is another type of scope called dynamic scope, and it assigns the value of variables at run time! Let's take a look at another program keeping in mind what we just learned.
let x;
x = 1;
function a() {
x = 2;
}
function b() {
let x;
a();
}
b();
// With Lexical scope, this will print 2
// With dynamic scope, this will print 1
console.log(x);
a();
// With Lexical scope, this will print 2
// With dynamic scope, this will print 2
console.log(x);
If we actually run this in lexically ("statically") scoped Javascript language, no matter which function we call, we will always print a value of 2 for x. This is because function a
will always reassign the variable x to a value of 2.
But with dynamic scope, we have to think in terms of call stacks. I know it is really confusing to do (hence why most languages aren't dynamically typed and why most people don't understand the Javascript this
keyword), but let's walk through it.
In this program, the call stack is first populated with the global scope x
variable, which is set to 1. We then call b()
, which will push the variable x
from the scope of function b()
to the call stack. Our call stack looks like this:
x (function b scope)
x (global scope)
Please note that although they are named the same variable, both x
variables occupy their own segment of memory and are assigned their own value.
So at this point, we call a()
, which sets x=2
.
But which x does it set??
In a lexically scoped language, we get to function a
and we don't see a variable declaration. Since there is no variable declaration, the compiler looks up one scope and finds x declared in the global scope. It then assigns this global x
variable to a value of 2.
With dynamic scope, the value of 2 is assigned to the variable x
which sits at the top of the call stack. If you remember, the x
in function b
scope sits at the top of the stack, which means that value of 2 is going to be assigned to it.
Therefore, when we print the value of x from the global scope, it is still a value of 1!
But things change a bit when we call a()
from the global scope. This time, our call stack looks like this:
x (global scope)
Therefore, the value of 2 will be assigned to the variable x
in the global scope, and we will print out a value of 2!
Rewind
That was a lot.
Why again are we here? Well, in order to understand the Javascript this
keyword, you have to get into the mindset of dynamically scoped variables. In order to understand dynamically scoped variables, you have to understand what statically scoped variables are. To understand statically scoped variables, you need to know what compilers do.
Sounds like a pretty big call stack of knowledge to me!
Anyways, to review:
- Javascript is a statically scoped language, which means variable values evaluate based on their "compile time" condition. Variables can evaluate "up a scope" but not "down a scope" (i.e. A nested function can use a global variable but a global function cannot use an encapsulated variable)
- The Javascript
this
keyword acts in a similar manner to dynamic scope, but it is not exactly the same. Nevertheless, understanding dynamic scope will help you understand thethis
keyword. - If you're completely lost, it may be the case that you are just not ready for this type of discussion yet. It took me years before I could understand many of these concepts, and it required lots of programming and practice to do so! If this is the case, you might revisit this article in the future.
Finally. The this
keyword explained
Just like dynamic scope depends on the order of the call stack at run time, the this
keyword depends on the call stack to determine which "context" this
is a part of.
There are 4 ways that this
can be "bound". We will start with the easiest and work our way to the hardest.
The new
keyword
This one is simple. When declaring a new instance of a function using the new
keyword, this
will always refer to the declared function.
function myFunction() {
var a = 2;
this.a = a;
}
var a = 4;
var functionInstance = new myFunction();
console.log(functionInstance.a); // 2
The this
keyword above refers to the myFunction
object, which assigns a property of a
which is equal to 2. Even though the call site of functionInstance
is in the global scope, the new
keyword overrides any rules regarding this
and explicity binds to the new function instance.
I consider this the easiest situation to identify what this
represents because it is so explicit.
Explicit Binding
This type of this
binding is very similar to the new
keyword, but in the case that you try to use both this method and the new
keyword at the same time, the new
keyword will take precedence.
There are actually multiple ways to explicitly bind the value of this
, but some are more outtdated than others. For simplicity, we will just look at one of these ways, which is the most common.
By using the bind()
prototype function that exists on all Javascript functions, you can explicity assign an object to represent the value of this
.
function myFunction() {
console.log(this.a);
}
var explicitlyBoundObject = {
a: 2,
};
var a = 4;
var functionInstance = myFunction.bind(explicitlyBoundObject);
functionInstance(); // 2
As with the new
keyword, explicit binding allows you to completely eliminate the idea of dynamic scope and call stacks out of your head and know exactly what this
represents.
Later, we will see that there are a few exceptions here, but for simplicity, take the above example at face value.
Default Binding
Default binding is a little trickier than the new
and explicit binding because there are a few nuances that you might not expect.
A good rule of thumb is this: If a function has been called in a "normal" fashion, then it has default binding and this
refers to the global scope.
When I say "normal", I'm referring to a function call that looks like this:
function myFunction() {
console.log("does something");
}
// Call function "normally"
myFunction();
There are only three other ways you could call this function, shown below:
var obj = {
myFunction: function () {
console.log("does something");
},
};
// Call function as a method
obj.myFunction();
function myFunction() {
console.log("does something");
}
// Call function using the call() method
// We have already covered -- `this` is bound to the function itself
myFunction.call();
function myFunction() {
console.log("does something");
}
// Call function as newly constructed object
// We have already covered -- `this` is bound to the function itself
var myFunctionObj = new myFunction();
myFunctionObj();
So if you see a function that is called "normally", you can reasonably assume that this
refers to the global object. The global object will be global
if using a NodeJS console, and window
if using a browser console.
In my opinion, there are two things that can throw a programmer off when thinking about default binding.
- "Strict" mode
-
const
keyword - Nested functions
Starting with "strict" mode:
function myFunction() {
"use strict";
console.log(this.a);
}
var a = 2;
myFunction(); // undefined
this
is undefined because using strict mode in Javascript makes the global scope unavailable. The purpose of strict mode is to force the developer to be conscious of scopes, security, and other best coding practices, and one of the ways this is implemented is by limiting the use of the global object.
Now, for the const
keyword:
function myFunction() {
console.log(this.a);
}
const a = 2;
myFunction(); // undefined
Using the const
keyword does not make the variable available on the global object. To see this in action, open up Google Chrome and go to the console. Type the following:
var a1 = 2;
const a2 = 2;
// In a browser, window is the global object
// In a NodeJS console, you would replace "window" with "global"
window.a1; // 2
window.a2; // undefined
And finally, nested functions:
function f1() {
function f2() {
var a = 6;
function f3() {
// Call Stack at this point in the program
// f3 (top)
// f2
// f1
// global (bottom)
console.log(this.a);
}
f3();
}
f2();
}
var a = 2;
f1();
With all this talk about call stacks and call sites, you might look at the above code and infer that this
represents something other than the global object. When this.a
is printed, the call stack has f3() at the top, which means that "call site" of f1()
is at f2()
. Said another way, even though f1()
is executed in the global scope, that does not mean its call site is in the global scope. The call site is in the scope of f2()
.
Knowing this, you might guess that the value of this.a
would be 6, since that is the value of a
at the call site of f1()
when this.a
is printed.
But this is not the case. Since f1()
is called as a "normal" function call, its scope will always be global, and therefore this.a
is equal to 2 in the above code.
Implicit Binding
And finally, the part where this
gets a little confusing. If we are calling a function as a property of an object, the value of this
is entirely based on the call site of the function.
var obj1 = {
color: "green",
func: () => {
console.log(this.color); // undefined
},
};
var obj2 = {
color: "green",
func: function () {
console.log(this.color); // green
},
};
obj1.func(); // undefined
obj2.func(); // green
In the above example, I have demonstrated the two concepts that you must understand for implicit binding of this
. Obviously, both of these functions are called from the global scope, but if you determine the real call site, it is within the context of each object, and therefore, the value of this
is the context object.
In the second function call, obj2.func()
, the results are unsurprising. We have determined the call site of this function to be the obj2
object, which has a property of color
equal to green.
The first function call is a bit confusing though, and it has to do with the syntax of the function property. In ES6, the fat arrow function was introduced. Unlike a normal function declaration, the this
keyword within a fat arrow function follows lexical (synonymous to "static") scoping rules as opposed to dynamic scoping rules where we have to look at call stacks and determine call sites to determine the value of this
.
Therefore, the value of this
in the fat arrow function is the global object, which does not have a property of color
.
Fat arrow functions' treatment of this
solves a problem for developers, best demonstrated by example.
function myAsyncFunction(callback) {
callback();
}
var obj = {
color: "green",
func: function () {
myAsyncFunction(function () {
console.log(this.color);
});
},
};
obj.func(); // undefined
Based on the previous examples, you might guess that this.color
is equal to green. But if you remember from the section on default binding, if we call a function "normally" (i.e. myAsyncFunction
has been called normally), this
will represent the global object. To solve this problem, Javascript developers have used something like the following:
function myAsyncFunction(callback) {
callback();
}
var obj = {
color: "green",
func: function () {
var self = this;
myAsyncFunction(function () {
console.log(self.color);
});
},
};
obj.func(); // green
By assigning the value of this
to a variable while we have access to it, we can pass it into the callback and use it.
Obviously, this is a contrived way to use this
. There is a better way, and it involves ES6 fat arrow functions:
function myAsyncFunction(callback) {
callback();
}
var obj = {
color: "green",
func: function () {
myAsyncFunction(() => {
console.log(this.color);
});
},
};
obj.func(); // green
Using this pattern requires a fairly deep understanding of the this
keyword, and makes you wonder why anyone would go to the trouble in the first place?
Why use this
in the first place?
After all this explanation, you might wonder why anyone would go to the trouble of using this
in their code?
Although entirely a personal opinion, I do not see an overly compelling reason to use the this
keyword while writing Javascript. Even if you become comfortable with the syntax, that doesn't mean that everyone that reads your code in the future will be comfortable with it. Sure, using this
has marginal benefits like code reuse, but I would much rather have a few extra lines of code that are highly intuitive than a codebase with a bunch of this
keywords that don't always behave as expected.
That said, there is a compelling reason to learn how this
works thoroughly. No matter how large a crusade you start against the use of this
in codebases, there will always be codebases that utilize it. Therefore, regardless of if you choose to implement this
in your codebase, you will certainly need to know how it works.
And with that, I hope this deep dive into the this
keyword has helped your understanding like it did mine.
Top comments (0)