Originally posted on my personal blog debuggr.io
In this article we will learn how to identify and recognize what this refers to in a given context and we will explore what rules and conditions are taken under consideration by the engine to determine the reference of the this key word.
You can also read this and other articles at my blog debuggr.io
The challenge
One of the most challenging concepts in JavaScript is the this key word, maybe because it is so different than other languages or maybe because the rules to determine it's value are not that clear.
Lets quote a paragraph from MDN:
In most cases, the value of this is determined by how a function is called (runtime binding). It can't be set by assignment during execution, and it may be different each time the function is called...
Challenging indeed, on one hand it says that this is determined at run-time - i.e, a dynamic binding, but on the other hand it says In most cases..., meaning it can be statically bound. How doe's something can be both static and dynamic and how can we be sure which one it is at a given context? This is exactly what we are going to find out now!
What is static?
Let's look at an example of something static in JavaScript, like the "Local variable environment" - often refers to as scope.
Every time a function is invoked, a new execution context is created and pushed to the top of the call-stack (when our application starts, there is already a default execution context which is often referred to as the global-context).
Each execution context contains a "Local variable environment" which usually referred to as the local-scope (or global-scope in the global execution context).
Given this code snippet:
function foo(){
var message = 'Hello!';
console.log(message);
}
foo()
Just by looking at foo's declaration, we know what scope message belongs to - the local scope of the foo function execution-context. Because var statement declares a function-scoped variable.
Another example:
function foo(){
var message = 'Hello';
{
let message = 'there!'
console.log(message) // there!
}
console.log(message) // Hello
}
foo()
Notice how inside the block we get a different result than outside of it, that's because let statement declares a block scope local variable.
We know what to expect just by looking at the deceleration of the function because scope in JavaScript is statically determined (lexical), or at "Design time" if you will.
No matter where and how we will run the function, it's local scope won't change.
In other words, we can say that the scope of a variable is depended on where the variable was declared.
What is dynamic?
If static means "Where something WAS declared", we might say dynamic means "How something WILL run".
Lets imagine for a moment that scope was dynamic in JavaScript:
note, this is not a real syntax ⚠️
function foo(){
// not a real syntax!!! ⚠️
let message = if(foo in myObj) "Hello" else "There"
console.log(message)
}
let myObj = {
foo
};
myObj.foo() // Hello
foo() // There
As you can see, in contrast to the static scope example we now can't determine the final value of message just by looking at the declaration of foo, we will need to see where and how its being invoked. That's because the value of the message variable is determined upon the execution of foo with a set of conditions.
It may look strange but this is not that far away from the truth when we are dealing with the this context, every time we run a function the JavaScript engine is doing some checks and conditionally set the reference of this.
There are some rules, and order matters.
You know what, lets just write them out as if we are writing the engine ourselves:
note, this is not a real syntax ⚠️
function foo(){
// not real syntax!!! ⚠️
if(foo is ArrowFunction) doNothing;
else if(foo called with new) this = {};
else if(
foo called with apply ||
foo called with call ||
foo called with bind ||
) this = thisArg
else if(foo called within an object) this = thatObject
else if(strictMode){
this = undefined
} else{
// default binding, last resort
this = window;
// or global in node
}
console.log(this); // who knows? we need to see where and how it runs
}
Seems a bit cumbersome and complex, maybe this flow chart will provide a better visualization:
As you can see we can split the flow into two parts:
- Static binding - The arrow function
- Dynamic binding - The rest of the conditions
Lets walk them through:
-
Is it an arrow function? -
If the relevant execution context is created by an arrow function then do nothing, meaning
thiswill be whatever it was set by the wrapping execution context. -
Was the function called with
new? -
When invoking a function with thenewkey word the engine will do some things for us:- Create a new object and set
thisto reference it. - Reference that object's
__proto__(called[[Prototype]]in the spec) to the function'sprototypeobject. - Return the newly created object (
this).
So for our purpose to determine what
thisis, we know it will be a new object that was created automatically just by invoking the function with thenewkey word. - Create a new object and set
Was the function called with
call/applyorbind? -
Then setthisto whatever passed as the first argument.Was the function called as an object method -
Then setthisto the object left to the dot or square brackets.Is
strict modeon? -
Thenthisisundefineddefault case -
thiswill reference the global / window.
The Quiz
The best way to measure our understanding is to test ourselves, so lets do a quiz. open the flowchart on a new tab and walk through it from top to bottom for each question (answers are listed below):
Try to answer what will be printed to the console.
Question #1
function logThis(){
console.log(this);
}
const myObj = {
logThis
}
myObj.logThis()
Question #2
function logThis(){
console.log(this);
}
const myObj = {
foo: function(){
logThis();
}
}
myObj.foo()
Question #3
const logThis = () => {
console.log(this);
}
const myObj = {
foo: logThis
}
myObj.foo()
Question #4
function logThis() {
console.log(this);
}
const myObj = { name: "sag1v" }
logThis.apply(myObj)
Question #5
const logThis = () => {
console.log(this);
}
const myObj = { name: "sag1v" }
logThis.apply(myObj)
Question #6
function logThis(){
console.log(this);
}
const someObj = new logThis()
Question #7
function logThis(){
'use strict'
console.log(this);
}
function myFunc(){
logThis();
}
const someObj = new myFunc()
Question #8
function logThis(){
console.log(this);
}
class myClass {
logThat(){
logThis()
}
}
const myClassInstance = new myClass()
myClassInstance.logThat()
Question #9
function logThis(){
console.log(this);
}
class myClass {
logThat(){
logThis.call(this)
}
}
const myClassInstance = new myClass()
myClassInstance.logThat()
Question #10
class myClass {
logThis = () => {
console.log(this);
}
}
const myObj = { name: 'sagiv' };
const myClassInstance = new myClass()
myClassInstance.logThis.call(myObj)
Bonus questions
Question #11
function logThis() {
console.log(this);
}
const btn = document.getElementById('btn');
btn.addEventListener('click', logThis);
Question #12
const logThis = () => {
console.log(this);
}
const btn = document.getElementById('btn');
btn.addEventListener('click', logThis);
Answers
Answer #1
function logThis(){
console.log(this);
}
const myObj = {
logThis
}
myObj.logThis()
Result - myObj.
Explanation:
- Is
logThisan arrow function? - No. - Was
logThiscalled withnew? - No. - Was
logThiscalled with call / apply / bind? - No. - Was
logThiscalled as an object method? - Yes,myObjis left to the dot.
Answer #2
function logThis(){
console.log(this);
}
const myObj = {
foo: function(){
logThis();
}
}
myObj.foo()
Result - window.
Explanation:
- Is
logThisan arrow function? - No. - Was
logThiscalled withnew? - No. - Was
logThiscalled with call / apply / bind? - No. - Was
logThiscalled as an object method? - No. - Is
strict modeon? - No. - default case -
window(or global).
Answer #3
const logThis = () => {
console.log(this);
}
const myObj = {
foo: logThis
}
myObj.foo()
Result - window.
Explanation:
- Is
logThisan arrow function? - Yes, whateverthisset in the wrapping context. In this case the wrapping context is the "Global execution context" which inside itthisrefers to the window / global object.
Answer #4
function logThis() {
console.log(this);
}
const myObj = { name: "sag1v" }
logThis.apply(myObj)
Result - myObj.
Explanation:
- Is
logThisan arrow function? - No. - Was
logThiscalled withnew? - No. - Was
logThiscalled with call / apply / bind? - Yeas, whatever passed in as the first argument -myObjin this case.
Answer #5
const logThis = () => {
console.log(this);
}
const myObj = { name: "sag1v" }
logThis.apply(myObj)
Result - window.
Explanation:
- Is
logThisan arrow function? - Yes, whateverthisset in the wrapping context. In this case the wrapping context is the "Global execution context" which inside itthisrefers to the window / global object.
Answer #6
function logThis(){
console.log(this);
}
const someObj = new logThis()
Result - The object created by logThis.
Explanation:
- Is
logThisan arrow function? - No. - Was
logThiscalled withnew? - Yes, thenthisis an auto created object inside the function.
Answer #7
function logThis(){
'use strict'
console.log(this);
}
function myFunc(){
logThis();
}
const someObj = new myFunc()
Result - undefined.
Explanation:
- Is
logThisan arrow function? - No. - Was
logThiscalled withnew? - No. - Was
logThiscalled with call / apply / bind? - No. - Was
logThiscalled as an object method? - No. - Is
strict modeon? - Yes,thisisundefined.
Answer #8
function logThis(){
console.log(this);
}
class myClass {
logThat(){
logThis()
}
}
const myClassInstance = new myClass()
myClassInstance.logThat()
Result - window.
Explanation:
- Is
logThisan arrow function? - No. - Was
logThiscalled withnew? - No. - Was
logThiscalled with call / apply / bind? - No. - Was
logThiscalled as an object method? - No. - Is
strict modeon? - No. - default case -
window(or global).
Answer #9
function logThis(){
console.log(this);
}
class myClass {
logThat(){
logThis.call(this)
}
}
const myClassInstance = new myClass()
myClassInstance.logThat()
Result - The object created by myClass.
Explanation:
- Is
logThisan arrow function? - No. - Was
logThiscalled withnew? - No. - Was
logThiscalled with call / apply / bind? - Yes, whatever passed in as first argument. OK, but we are passingthis! what isthisrefers to inside thelogThatexecution context? Lets check:- Is
logThatan arrow function? - No. - Was
logThatcalled withnew? - No. - Was
logThatcalled with call / apply / bind? - No. - Was
logThatcalled as an object method? - Yes,thisis the object left to the dot - The auto created object insidemyClassin this case.
- Is
Answer #10
class myClass {
logThis = () => {
console.log(this);
}
}
const myObj = { name: 'sagiv' };
const myClassInstance = new myClass()
myClassInstance.logThis.call(myObj)
Result - The object created by myClass.
Explanation:
- Is
logThisan arrow function? - Yes,thisrefers to whatever the wrapping context set it,myClassin this case. Lets check whatthisrefers to in the wrapping context:- Is
myClassan arrow function? - No. - Was
myClasscalled withnew? - Yes,thisrefers to the newly created object (the instance).
- Is
note that we are using class fields which is a proposal currently in stage 3
Answer #11
function logThis() {
console.log(this);
}
const btn = document.getElementById('btn');
btn.addEventListener('click', logThis);
Result - The btn element.
Explanation
This is a tricky question because we never talked about event handlers attached to DOM elements. You can look at event handlers that are attached to DOM elements as if the function is a method inside the element's object, In our case the btn object. We can look at it as if we did btn.click() or even btn.logThis(). Note that this is not exactly whats going on under the hood, but this visualization of the invocation of the handler can help us with the formation of our "mental model" regarding the setting of this.
You can read more about it on the MDN
Now lets walk through the flow:
- Is
logThisan arrow function? - No. - Was
logThiscalled withnew? - No. - Was
logThiscalled with call / apply / bind? - No. - Was
logThiscalled as an object method? - Yes (sort of), in our casebtnis left to the dot.
Answer #12
const logThis = () => {
console.log(this);
}
const btn = document.getElementById('btn');
btn.addEventListener('click', logThis);
Result - window.
Explanation
- Is
logThisan arrow function? - Yes, whateverthisset in the wrapping context. In this case the wrapping context is the "Global execution context" which inside itthisrefers to the window / global object.
Wrapping up
We now understand that the assignment of this can be both dynamic and static (lexical).
- Arrow functions will make it static and won't even bother to mutate
thisat all. which means we will need to understand whatthiswas set to in the wrapping execution context. - Plain Functions will make it dynamically, meaning it depends on how the function was invoked.
It may look intimidating and complex now, you probably thinking how would you remember the flow chart. Well you don't need to, you can save or print this flow-chart or maybe even make your own. Every time you need to know what this refers to in your code just look at it and start going through the conditions. Rest assure, you will need to look at this flow-chart less and less as time goes by.
I hope it was informative and helpful, if you have any further clarifications or corrections, feel free to comment or DM me on twitter (@sag1v).
You can read more of my articles at my blog debuggr.io

Top comments (4)
That's the best explanation I've found so far. Thank you
Thank you! Im glad you found it helpful
I just try to avoid using
thisaltogether, always seemed more trouble than is worth! But maybe that's an advantage of a functional style.For question 11 :
ele.addEventListener('click', handler) ;
handler's this here never referes to ele but to someone else who calls it which is 'currentTarget' unless you stop propagation :
ele.addEventListener('click', handler,false) ; // handler's this referes to ele.
The case of using 'this' in event handlers is very special and frustrating
.. I assume that the handler is being called outside the method 'addEventListener' which is owned by 'ele' and it forces the handler's 'this' to refer to the owner ..
I liked the checking list and saved it 🙂