This article is to help you deduce the value of 'this' in javascript. It is not as simple as in Java where this refers to the current object. In javascript, "this" is dynamic. To calculate the value of this, the following 3 tips are enough and it is not difficult at all. I will present the tips and give some sample snippets in the end to help you understand the idea better.
(Basic knowledge of JS is assumed)
The first tip:
Javascript is a function-scoped language.
It does not create new lexical scopes for every bracket pair. This is a common misunderstanding.
The following syntax does not create a new scope:
if (true) {
// statements
}
Same applies for loops, switch statements as well.
However the following statement does create a new scope:
function getName() {
return "Jake";
}
Note the use of function keyword here. Arrow functions do not create a new scope.
The second tip:
Starting from ES6, there are two variants for creating functions in JS:
- Using function keyword
- Using arrow syntax
The important difference between them is arrow function is very lightweight - it does not support prototype keyword; bind, call and apply don't work, arrow functions are not constructible and arrow functions do not create a scope.
However the most important distinction lies in how they both handle this keyword.
- this keyword inside a normal function is bound to the object where the reference of the function is invoked.
Note: If there is no outer scope, the default scope is used, which is the global object (Window in case of browser and global in case of Node.js)
function getName() {
return this.name
}
// will return Window.name because getName is called globally.
getName();
One catch is "this" of global scope will be set to undefined in strict mode. (but not really the point of focus here)
- this keyword inside an arrow function is bound to the object where the function is defined.
Note the difference between defined and invoked. More examples on this after the third one.
The third tip:
The function keyword is special. It sets its scope to the object even if it is defined using the object literal or function is defined using prototype property. And more special property of normal functions: nesting normal functions does not alter how this is resolved. Every nested function is simply treated as a top level function.
object literal syntax:
let obj = {
fn: function() {
// prints obj
console.log(this)
}
}
However, the function using arrow syntax does not comply with the above rule (because remember js is function-scoped and not bracket-scoped).
let obj = {
fn: () => {
// prints Window (outer scope in this case)
console.log(this)
}
}
functional literal syntax:
The extension to this rule is when you define object using the function literal.
Consider a car class
function Car() {
this.name = "BMW";
}
Car.prototype.getName = () => this.name;
const c = new Car();
// Will return Window.name
c.getName();
the getName is defined using arrow syntax and thus does not obey the prototype declaration and prints out Window.name
However,
Car.prototype.getName = function () {
return this.name;
}
will return "BMW". This is because of the nature of function keyword is to obey prototype declaration or object literal.
What happens in ES6 class syntax?
You might know this already, ES6 classes are just a sugarcoat over the function literal for defining objects.
class Car {
name = "BMW";
getName() {
return this.name;
}
}
The above getName will return BMW because function keyword obeys function literal objects.
Arrow syntax:
class Car {
name = "BMW";
getName = () => {
return this.name;
}
}
The arrow syntax also prints BMW because of a different and an interesting reason - since class keyword just abstracts function literal and function literal creates a scope, the getName arrow function is always bound to Car object. This is different to object literal arrow syntax case - where it was bound to the outer scope and not the object itself.
And that's it!
These are the three tips which you can follow to always deduce the exact value of this keyword.
Building up on the above idea, let's consider the below examples:
Example 1: Indirect invocation
class Car {
name = "BMW";
getNameFn() {
return this;
}
getNameArrow = () => {
return this;
}
}
function printUtil(obj) {
const util = (fn) => {
console.log(fn());
}
util(obj);
}
let c = new Car();
printUtil(c.getNameFn); // prints undefined
printUtil(c.getNameArrow); // prints BMW
If we pass a function defined inside a class to another function, the scope will change as in the example above, the first print statement produces undefined.
However, if we define an arrow function, it is always bound to where it is defined (respecting the function-scope) and thus it prints BMW.
In order to overcome this situation, js has methods like bind, apply and call which indirectly invoke the function.
printUtil(c.getNameFn.bind(c)); // prints BMW
bind, call and apply are simple. They just call the function with the scope of any given object (This prevents function from having dynamic "this" value). Here, c.getNameFn is passed to printUtil and is bound to object "c" (It can be bound to any object for that matter). Therefore it prints BMW.
Example 2: Function Invocation
function print() {
console.log(this)
}
print()
Since print function is invoked directly, it will print its outer scope object which is Window object.
Example 3: IIFE
(function () {
console.log(this)
})()
This syntax is called Immediately Invoked function expressions (IIFE). Quite a mouthful but nothing special about them. Consider them as normal functions that get invoked.
Thus the value of this will be its outer scope object. Same as above.
Example 4: Nested Function
let obj = {
name = "car";
function print() {
function util() {
console.log(this); // prints Window
}
util();
}
}
obj.print()
The one caveat here is nesting normal functions does not modify how this resolves. (In other words, closure scope does modify this). Every nested function is simply treated as a top level function. Therefore util is still treated as a separate function and will print Window.
But as you may have guessed, this is still dynamic. You can bind it to print function using the two methods as discussed earlier:
- use bind/call/apply keyword:
let obj = {
name = "car";
function print() {
function util() {
console.log(this); // prints obj
}
util.call(this);
}
}
obj.print()
- Or use the arrow syntax:
let obj = {
name = "car";
function print() {
const util = () => {
console.log(this); // prints obj
}
util.call(this);
}
}
obj.print()
The reason: You guessed it! JS is function-scoped and print function created a scope and since the scope of util arrow function is based on where it is defined (respecting function-scope).
Conclusion:
- Always remember JS is function-scoped. It will solve a lot of this keyword confusions.
- The function keyword obeys function literal and object literal.
- The arrow syntax obeys only function-scope nature of js.
- Nesting normal functions do not modify how this is resolved.
- If the value of this inside a normal function has to be bound to a specific object consider bind/call/apply.
- Always prefer ES6 classes and Arrow syntax. Go for normal functions only when you have to extend a prototype.
More resources:
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/this
https://dmitripavlutin.com/gentle-explanation-of-this-in-javascript
Feedbacks, questions and constructive criticisms are always welcome!
Top comments (0)