this keyword in JS can be trickier to understand at first glance. The obvious reason could be the way the this keyword behaves in JavaScript is different when compared to its peer languages like C++, Java, etc
But to be honest, understanding the this keyword in JavaScript is actually easier than it looks.
There are some fixed sets of rules provided by ECMA. So if we understand those rules, the this keyword is done and dusted.
By the end of this post, I assure every reader will gain complete knowledge of the this keyword in JavaScript and will be able to use the this keyword more confidently in their projects.
Now starting, before actually diving into the this keyword and understanding its behavior, we have to know a few prerequisites, and these prerequisites would majorly clear a lot of dust around the this keyword in JS.
Prerequisites:
- The
thiskeyword has a valid meaning only inside an execution context.
Now what does that mean?
JS has primarily 2 execution contexts defined in the language. (Actually, there are 4, but for the moment, we will skip the other 2 execution contexts (Eval, Module)). They are
- Global Execution Context
- Functional Execution Context
So there's the goal. Understanding this in the above two mentioned execution contexts will help us understand 95% of the this behavior in JS
1. this in Global Execution Context:
-
Standard Rule:
thisin GEC (Global Execution Context) will always represent the global object
Now what's this global object? The global object is provided by the Host
If you remember, JS, or more specifically, ECMAScript, is just a specification/contract of the language, and the JS engines (V8, SpiderMonkey, etc.) are the actual software that implements this specification.
These engines convert the JS code to machine-level code (binary).
And these JS engines are usually embedded inside the host.
The host can be a browser (Chrome, Safari, etc.), Node.js, Bun, etc
- So, based on the host, the global object might differ, and so might the value of the
thiskeyword.
So the value of the this keyword when checked on the browser console vs. on Node REPL would differ.
But the main point is that the this keyword in GEC would always hold the value of the global object
As we can see, this inside a browser gives a window object (as the window object is the global object inside a browser), but the this inside Node REPL gives a different object with different properties.
Note:
One of the very common misconceptions among many developers is that they assume the global object in Node.js is {}
Because when we try to check the value of this in a file and run that file via Node.js, we see {}
But there's a catch: a global object cannot be empty, and this is clearly evident when we tried to see the value of this in Node REPL.
Then why are we seeing an empty object {}?
The answer lies in how Node.js works internally. Any code that you write inside a file run by Node.js, Node.js will never run that file in the true global scope.
Now, why's that? Before ES6, JS developers had only the var keyword to declare variables, and variables declared with the var keyword are stored in the global scope.
So, if 2 files in a project declare the same variable using the var keyword, it would pollute the global scope. Meaning the variable declared with var would override its previous entry.
Ex: Let's say a project has 2 files named f1.js and f2.js.
If f1.js has declared a variable named with hasPermission with the value true and f2.js has also declared a variable with the same name hasPermission with the value false, and assuming f1 runs first and f2 later, the final value of the hasPermission variable would be false, as running the f2.js file has overridden the value of the hasPermission variable.
Now the results of the file f1.js would be catastrophic, as the whole logic of f1.js would be affected by this overriding, as all the variables declared by the var keyword are stored in global scope.
This was a big problem.
Hence, Node.js provided a solution of its own.
Node.js always wraps your file inside a function, and that function would be internally invoked so that every variable defined with the var keyword will live only inside that file, as the function will create a separate execution context, hence saving the global scope from being polluted.
That also means that any code that is in a file when run by Node.js will be wrapped in a function, and the code truly doesn't run in the global scope of Node.js
But still, the question is unanswered. Why does this give an empty object? This will be answered in the upcoming sections of the blog...
2. this in Functional Execution Context:
As we all know, functions in JS create a new execution context; hence, the
thiskeyword will behave differently inside a function.
But how different?Standard Rule: The value of
thisinside a function will depend on how the function is invoked.
Now that leads to a question: In how many ways can we invoke a function in JS?
The answer is 4. They are:
- Function invocation (the plain function call)
- Method invocation (via an object reference)
- Explicit Binding (using call/apply/bind methods)
- Constructor invocation (using the new keyword)
So depending on which type of invocation is used, this will be populated correspondingly.
Now let's dissect each invocation style.
1. Function invocation:
- The function invocation is the most familiar one. And also simple to understand. A function, when invoked directly by its name and a set of paranthesis
Ex:
function wish() {
console.log('Hey there !')
}
wish()
- In the case of a plain function call,
thiswill always default to the global object. But this is true only when the code is running inside non-strict mode.
NOTE:
ES5 introduced a new mode to JS called strict mode. As the name suggests, running the JS code in strict mode will not allow a few things that otherwise run normally in non-strict mode
And one such thing is defaulting to the global object
So if the code is running in strict mode,
thiswill not be defaulted to the global object, and it will beundefinedStandard Rule: In the case of function invocation, the value of
thiswould be the global object if running in non-strict mode andundefinedwhen running in strict mode
function f1() {
console.log(this) // prints the global object of Node when running in true global scope of Node. For ex: in Node REPL
}
f1()
'use strict';
function f2() {
console.log(this) // undefined
}
f2()
The output in the 1st case would be the Node.js global object, whereas in the 2nd case, the output would be undefined
2. Method invocation:
- This is the case when a function is present inside an object. And to invoke that function, we need the object reference. Ex:
const obj = {
name: 'Peter',
wish: function() {
console.log(this.name, ' says hi');
}
}
obj.wish();
Standard Rule: In the case of method invocation,
thiswill become the object on which the function is called.So in our above example, since wish is called on obj, inside wish,
thiswill point to the obj itself, and thereforethis.namewill have the valuePeter
We can also verify this by printing this inside the wish method. It will give the object itself.
const obj = {
name: 'Peter',
wish: function() {
console.log(this) // `this` will point to the obj itself
console.log(this.name, ' says hi');
}
}
obj.wish();
Gotcha:
Just think of what the output of the following code snippet would be ?
const obj = {
name: 'Peter',
wish: function() {
console.log(this.name, ' says hi');
}
}
const wish = obj.wish;
wish()
If you have guessed undefined, good job; you have guessed it right.
Now why's that?
Always remember the golden rule. this inside a function will always get its value based on how it is invoked.
In our above example, it doesn't matter that the wish variable is getting its value from obj.wish, what matters is how the function is invoked at the end of the day. Here it is, a plain invocation (Case 1)
So in the case of plain invocation, we need to check whether the code is running in strict mode or not. Since we aren't using 'use strict;', our program is running in non-strict mode, and this will default to the global object.
Now, based on the host, the global object would differ. Assume the host is Node.js. In the Node.js global object, we do not have any property named name, hence we get undefined.
The same explanation can be continued if the above program is running in a browser. In the browser, the global object is window, and the window object doesn't have any property named name, hence even in the browser, this.name will yield to undefined
If the same code had run in strict mode, it would have led to a runtime error. And I hope you can guess the reason. As in strict mode, this becomes undefined, and accessing .name on undefined would lead to a runtime error
3. Explicit Binding:
- This is a special case where we bind the
thisvalue to a specific value and then invoke the function with our giventhis. (This might sound confusing at first glance but will be cleared very soon.)
And to do this binding, JS has given us 3 methods.
They are:
- Call
- Apply
- Bind
These 3 methods are available to every custom function we define and help us to invoke a function with custom this
Ex:
function demo() {
console.log('This is a demo function');
}
demo.call(null)
Do not focus on the null that is passed, but the point being, we can invoke the call method on demo.
Now let's see in detail about each of these methods.
i) Call:
Syntax:
call(this, arg1, arg2, ...)
function wish(msg) {
console.log(`${this.name} says ${msg}`)
}
const obj1 = { name: 'Peter' }
const obj2 = { name: 'Tony' }
wish.call(obj1, "hi");
wish.call(obj2, "hey there");
Now, as you can see, we have reused the same function by passing different contexts.
For the first console log, we get Peter says hi
And for the second console log, we get Tony says hey there
The first argument we pass to call is considered as this and same would be used inside the function.
ii) Apply:
"Apply" is the same as "call." Just that the way arguments are passed to functions differs.
Syntax:
apply(this, [arg1, arg2,...])
Ex:
function wish(msg) {
console.log(`${this.name} says ${msg}`)
}
const obj1 = { name: 'Peter' }
const obj2 = { name: 'Tony' }
wish.apply(obj1, ["hi"]);
wish.apply(obj2, ["hey there"]);
As you can see, the only difference is that the argument list is passed in an array instead of individual values
After the introduction of the spread operator in ES6, all the places where apply is used can be swapped with call using the spread operator
Ex: wish.call(obj1, ...["hi"])
iii) Bind:
Bind is the most interesting of all. In the case of call and apply, the function is invoked immediately with the context we pass.
But in the case of bind, the function isn't invoked immediately; rather, bind returns a new function with the passed context, and whenever this new function is invoked, the original function runs with the bound context.
Syntax:
bind(this, arg1, arg2, ...) => returns a new function
Ex:
const person = {
name: "John"
};
function greet() {
console.log(`Hello, ${this.name}`);
}
const boundGreet = greet.bind(person); // returns a new function and the returned function will always have `this` pointed to `person`
boundGreet(); // Hello, John
// At the end function greet runs upon boundGreet(), but `this` is binded to person
NOTE:
Bind permanently binds this context, meaning you cannot override the context later.
Ex:
const person = {
name: "John"
};
function greet() {
console.log(`Hello, ${this.name}`);
}
const fn = greet.bind(person);
fn.call({ name: "Mike" }); // fn has `this` permanently binded to person, and cannot be overridden, hence we see the output as Hello John
Bind can be very useful in the case of callback functions so that the callback functions don't lose the this reference.
Ex:
const user = {
name: "Peter",
sayHi: function() {
console.log(`Hi, ${this.name}`);
}
};
setTimeout(user.sayHi, 1000);
Try to guess the output of the above code snippet. The answer might surprise you at first.
The output of the code is Hi undefined if running in non-strict mode; else, a runtime error if running in strict mode
You might say, Hey, this happens in the case of a plain invocation call, and that's a perfect observation.
Even though sayHi is invoked on the user object (method invocation - case 2), what we have to observe is that it is passed as a callback function
And now setTimeout has full control over this callback function, and once the timer expires (which is 1000 ms in this case), setTimeout will invoke our callback function
setTimeout would roughly look like (not exact code, but generic pseudo code)
function setTimeout(cb, timer) {
/*
Register the timer in node process and wait till timer expires
*/
// once the timer expires,
cb() // this is the most important thing to observe, now try to guess what this is ? This is the plain function invocation (Case 1)
}
And that's the reason the output we saw in the earlier code snippet produced output as if it was invoked directly (plain invocation)
Now if I ask you to fix this code so that when the callback runs, we should get the output as Hi, Peter
It's simple if we ask what we need?
We need a function that won't execute immediately and also has a permanent this binding. And that's what bind does.
Fixing the above code snippet.
const user = {
name: "Peter",
sayHi: function() {
console.log(`Hi, ${this.name}`);
}
};
const sayHi = user.sayHi;
const fn = sayHi.bind(user); // now fn has a permanent this binding to user and sayHi won't execute immediately at this moment of `bind`
setTimeout(fn, 1000); // Hi, Peter
Gotcha:
Remember we left a question unanswered. Why does this give {} as a result in a Node.js file?
As we already discussed, this is because of Node.js behavior; our code isn't running in true global scope, as Node.js wraps our file inside a function and internally calls this function using the call method.
And to this call method, the first argument passed is module.exports. Initially, when a file is run, module.exports is empty ({})
And we also know that the first argument to call is the this we set, and this is the reason why this gives an empty object in a Node.js file.
Under the hood, code is wrapped in a function. And that function is called via call(module.exports,arg1,arg2 etc)
console.log(this)
So a file like this in Node.js becomes
(function (exports, require, module, __filename, __dirname) {
console.log("Hello");
});
// and the above function is invoked by Node in the following way
/*
wrapperFunction.call(
module.exports, // this ({})
module.exports, // exports
require, // require
module, // module
__filename, // filename
__dirname // dirname
);
*/
4. Constructor invocation:
- The final way to invoke a function is to invoke it using the
newkeyword.
Syntax:
new fn()
- The new keyword broadly does 4 things in the background. They are:
- Create a brand new object
- Make
thispoint to this new object - Set the new object's prototype (proto property) to the function's prototype
- Run the constructor function and return this new object (inbuilt return) if no explicit return is mentioned.
Meaning now this is pointed to the object created because of the new keyword
Ex:
function User(name,age) {
this.name = name;
this.age = age;
}
const u = new User("Peter", 25);
console.log(u.name) // Peter
console.log(u.age) // 25
Note:
To understand the constructor invocation completely, we also need to have knowledge of objects and prototypical inheritance, but we will skip this in this blog, as this is purely focused on the this keyword
- When the instruction
const u = new User("Peter", 25)runs, all the above 4 activities mentioned will run, and finally an object will be returned from the user function (internally), and variableunow holds the reference to the object.
Gotcha
What will happen if we call the user function directly without the new keyword?
This will lead to plain function invocation (Case 1), and depending on whether the code is running in strict vs. non-strict mode, the output might differ.
In strict mode, the program will run into a runtime error, as this is undefined
But in non-strict mode, this will default to the global object, and name and age properties will be added to the global object
function User(name, age) {
this.name = name;
this.age = age;
}
const u = User("Peter", 25)
console.log(u) // undefined. without new keyword, there is no implicit return, hence default value undefined is returned
console.log(globalThis) // (globalThis refers to global object of the host(Browser/Node.js etc)) now globalThis will have name, age properties on it as code is running in non-strict mode by default
So these are the 4 cases inside functions where the this value always changed because of how the function was invoked.
Special Cases:
1. Arrow function:
ES6 introduced a new type of function named arrow functions.
The 2 main reasons why arrow functions were introduced were:
- To reduce verbose code
- To hold
thisbased on the place they were declared
Now in the blog, we will focus on the use case related to the this keyword
Golden rule: Arrow functions always capture this at the time of creation from the nearest enclosing execution context.
In other words, arrow functions do not have their own this
Syntax:
() => {}
Let's understand the above statement in detail. But before that, let's understand the problem that existed in the first place
Now we know that every function in JS has an execution context, which has an impact on the this value
Take the following example:
const user = {
name: "Peter",
greet: function() {
setTimeout(function () {
console.log(this.name);
}, 1000);
}
};
user.greet();
Now we know that when the greet function is invoked, first the timer runs; post timer expiry, the callback function will be invoked as a plain function call from the setTimeout function, which leads this to either take the global object in non-strict mode or undefined in strict mode.
And we know bind is one solution to solve this problem.
But arrow functions also solve this problem, that too effortlessly compared to bind
Rewriting the same example
const user = {
name: "Peter",
greet: function() {
setTimeout(() => {
console.log(this.name);
}, 1000);
}
};
user.greet();
Now remembering the golden rule, arrow functions capture this on the moment of creation from the nearest enclosing execution context
So the nearest enclosing execution context to our arrow function is the greet function (as functions in JS create execution contexts).
So at the time of creation itself (during the setTimeout instruction), the arrow function will capture this from the greet function
Now what would be the value of this in the greet function?
That depends on how the greet function is invoked. It's invoked by user reference (Case 2), so this will point to the user object itself.
And this this value is captured by the arrow function.
So when the arrow function actually runs after the timer expires, it already has this with it (captured at the time of creation), and this.name prints Peter after 1 ms.
Example 2:
const wish = () => {
console.log('Hi ', this.name)
}
Try to guess the output. (Assume the host is Browser.)
If you guessed the output as Hi, undefined, excellent, you have understood this in arrow functions.
Again, to answer this, let's remember the golden rule: arrow functions capture this at the moment of creation from the nearest enclosing execution context.
To the wish function, the nearest enclosing execution context is the global execution context, and in the GEC, this holds the value of the global object. And since here the host is a browser, the global object is the window object.
And in the window object, we do not have any property named name, hence .name gives undefined.
Exercise:
Try to guess the output of the same program when the host is Node.js (when run in true global scope vs. when run via file)
Answer: In both cases, .name is undefined, but in true global scope, this holds the value of the node's global object
And when run via file, this has the value of an empty object ({}).
2. Combination of invocations in a functional execution context:
Now we know that this inside a function gets a value based on how the function is invoked.
And we also saw that there are broadly 4 ways to invoke a function in JS.
But what happens when a function is invoked using a combination of these four approaches?
Let's take an example:
const user = {
name: 'Peter',
wish: function(msg) {
console.log(`${this.name} says ${msg}`);
}
}
const obj1 = { name: 'Tony' };
const obj2 = { name: 'Steve' };
user.wish.call(obj1, "hey there"); //combination of case 2 and case 3
const bound = user.wish.bind(obj2); // combo of case 2 and case 3
bound("on your left")
Now what will be the output of this program?
If we observe, the wish function is invoked by a combination of method invocation using object reference (case 2) and explicit binding (case 3).
Now what should be the value of this ? There's a conflict.
To resolve these conflicts, ECMA has given a precedence order.
Precedence order:
Constructor invocation > Explicit binding > Method invocation > Plain invocation
So according to the binding precedence rules, explicit binding (using call, apply, or bind) takes priority over method invocation.
Hence, the output of the above program would be:
Tony says hey there
Steve says on your left
Ex 2:
function Person(name) {
this.name = name;
}
const obj = {
name: "Bound Object"
};
const BoundPerson = Person.bind(obj);
// Using `new`
const p = new BoundPerson("Actual Person");
console.log(p.name);
Ideally, bind creates a permanent binding, making boundPerson function to point this to obj, but as per the precedence rules, the new operator takes more precedence.
And hence, this will point to the object returned by the new keyword, leading to the output Actual Person
Exercise: Try running the 4 steps that run in the background when the new operator is used
3. Module scope:
Remember at the start of the blog, we learned that JS has 4 execution contexts defined in the language
And we discussed in detail regarding this behavior in Global Execution Context (GEC) and Functional Execution Context (FEC).
The remaining contexts are:
- Eval
- Module
We’ll skip eval, since its use is widely discouraged in modern JavaScript.
Understanding this in module scope is pretty easy.
Module scope, introduced in ES6, ensures that each JavaScript module has its own isolated scope, separate from other files.
Standard rule: Modules always run in strict mode by deafult.
Meaning we don't have to explicitly mention "use strict";
If we recollect the case-1 of FEC, i.e., the plain invocation, this always had a possibility of 2 values.
Global object in non-strict mode and undefined in strict mode.
But running the same file as a module cuts down the option to default to a global object, as they always run in strict mode
function wish() {
console.log(`${this.name} says hi`);
}
Now try to guess the output of this program.
We have spent a lot of time understanding this in FEC. By this time, you could easily spot that this is plain invocation (case 1)
In case 1, we always have the scope to get 2 answers: either a global object (if running in non-strict mode) or undefined (if running in strict mode).
Now, since we know that modules by default runs in strict mode, the output is this becomes undefined and access to .name on undefined leads to a runtime error.
NOTE:
In JS, we can create a module by naming the file with the .mjs extension
Alternatively, by setting the type property to "module" in package.json, the entire project is treated as ES modules and runs in strict mode.



Top comments (0)