This is awkward in JavaScript
Whale! This is awkward in JavaScript, also many things in life are. But nothing to worry about, we will try to overcome “this” awkwardness in this article.
This keyword is there in many languages especially those who are revolving around Object Oriented concepts. So nothing untold or mysterious phenomena of JavaScript alone, it’s a very day to day topic which every one (of course JS devs) should be aware of. Before getting our hands dirty (coding) we will first checkout a famous quote by the legend Bruce Lee,
Be like water making its way through cracks. Do not be assertive, but adjust to the object, and you shall find a way around or through it. If nothing within you stays rigid, outward things will disclose themselves.
Empty your mind, be formless. Shapeless, like water. If you put water into a cup, it becomes the cup. You put water into a bottle and it becomes the bottle. You put it in a teapot, it becomes the teapot. Now, water can flow or it can crash. Be water, my friend.
- Bruce Lee
JavaScript’s this keyword follows the above saying as is. It behaves like water and it adapts to the object/element/environment in which it is being used. So the idea is pretty simple, in order to understand this keyword we have to understand the context (object/element/environment) in which it will be used.
JavaScript This keyword will be used with the following contexts:
- this with Implicit Binding
- this with Explicit Binding — call(), apply() and bind()
- this within Constructor Function/Classes
- this with inside HTML Event Listeners
- this with Global Object
- this with Strict Mode
- this with Arrow Functions
1. this with Implicit Binding
In case of implicit binding JavaScript binds the property of an object to an appropriate instance implicitly.
const spidey = {
firstName: 'Peter',
lastName: 'Parker',
fullName: function () {
return `${this.firstName} ${this.lastName}`;
}
};console.log('Spidey:', spidey.fullName());// Spidey: Peter Parker
In the above code, the fullName() method returns concatenated values of firstName and lastName properties. We have used this keyword for accessing the firstName and lastName properties, here JavaScript will simply bind this to the current object which is the hero object as it has been used to involve the fullName() method.
So we can simply conclude that inside any object method by default this keyword will refer to the object which is used to invoke the method.
2. this with Explicit Binding — call(), apply() and bind()
In case of explicit binding in JavaScript, the binding of the property to an instance is dependent on — for which object the method is invoked. The explicit binding is achieved by call(), apply() and bind() functions.
function areaOfRectangle() {
return this.length * this.width;
}const rectangle1 = { length: 2, width: 2 };
const rectangle2 = { length: 3, width: 2 };const area1 = areaOfRectangle.call(rectangle1);
const area2 = areaOfRectangle.apply(rectangle2);console.log('Area 1:', area1);
console.log('Area 2:', area2);// Area 1: 4
// Area 2: 6
In the above code, the areaOfRectangle() function returns the area of a rectangle by multiplying the length and width of current instance properties. We have created 2 rectangle objects rectangle 1 and rectangle 2 for which we want to invoke the areaOfRectangle() function. In order to invoke methods on behalf of some other object there exist functions named call() and apply(). We are making a call to the areaOfRectangle() function using the call() function for rectangle1 and apply() function for rectangle 2.
Note that both call() and apply() functions do the same thing only the difference is that if the calling function contains parameters using call() we need to pass those parameters comma separated whereas in apply we can pass an array of parameters.
There is one more player in the game which is the bind() function which sort of does the same thing as call() and apply() but has a glorious purpose. The bind() function creates the same function on which it is called, but this keyword within the newly created function will specifically point to the supplied object. One important aspect is that you cannot use the spell twice, meaning you cannot call the bind() function on a function over which the bind function has already been called. It is not an error, instead it will return the same value which the first binded function returns. Checkout below code, consider the same areaOfRectangle() function:
const rectangle3 = { length: 4, width: 2 };const areaOfRectangleRevised = areaOfRectangle.bind(rectangle3);console.log('Area 3:', areaOfRectangleRevised());const rectangle4 = { length: 5, width: 2 };const areaOfRectangleRevisedRevised = areaOfRectangleRevised.bind(rectangle4);console.log('Area 4:', areaOfRectangleRevisedRevised());// Area 3: 8
// Area 4: 8
We have created a third rectangle object for which we are creating a new function named areaOfRectangleRevised() using the bind() function and with rectangle 3 object. Now the newly created areaOfRectangleRevised() function’s this keyword will point to the rectangle 3 object as per the above mentioned law.
Now if we try to use the bind() function twice on the revised function and create a new areaOfRectangleRevisedRevised() function using the same technique but for rectangle 4 object, it will still returns the same result as rectangle 3 and not for rectangle 4.
So we can simply conclude that using explicit binding techniques (call, apply, bind) we can decide the future of this keyword inside the called function.
3. this within Constructor Function/Classes
Classes introduced in JavaScript in ES6 (ECMAScript 2015):
class Book {
constructor(title, author) {
this.title = title;
this.author = author;
}
};
JavaScript developers before 2015:
function Book(title, author) {
this.title = title;
this.author = author;
};
Classes are just syntactic sugar for constructor functions, so let us simply focus on the keyword this and will not bother about the creation of the Book class. Use either of the above approaches. Checkout the below Book objects and how this keyword behaving:
const book1 = new Book('Harry Potter', 'J. K. Rowling');
const book2 = new Book('A Game of Thrones', 'George R. R. Martin');console.log('Book 1:', book1);
console.log('Book 2:', book2);// Book 1: Book {title: "Harry Potter", author: "J. K. Rowling"}
// Book 2: Book {title: "A Game of Thrones", author: "George R. R. Martin"}
This keyword in constructor function or class creates the class members/properties using the same name as the function parameter. So in the above example the title and author are 2 parameters of the Book function and this.title and this.author are the actual object’s properties.
So we can simply conclude that inside the constructor function or class this keyword refers to the object which is initialized using the new keyword.
4. this with inside HTML Event Listeners
Consider the below HTML button; we have declared a button with click1 as id.
<button id="click1">Click 1</button>
Checkout the JavaScript code for the above HTML:
const click1 = document.getElementById('click1');click1.addEventListener('click', function () {
console.log('Click 1 Handler: ', this);
});click1.click();// Click 1 Handler: <button id="click1">Click 1</button>
Inside the addEventListener() method we are making use of this keyword, here it will refer to the element over which the specified event is applied; in above case the click event.
So we can simply conclude that inside the addEventListener() method this keyword refers to the element over which the specified event is applied. So we can simply make use of this for element’s properties like classList, style, innerHTML etc.
5. this with Global Object
When this keyword is used directly on the root level of the JS file, then it simply returns the global Window object.
console.log('Root Level:', this);// Root Level: Window {window: Window, self: Window, document: document, name: "", location: Location, …}
In the case of Node.js if we use this directly on the root level of any file, it simply returns an empty object. But if we access this keyword inside a root level function and then call that function we will get an export object which Node.js uses to initialize the root level Node.js object. This simply gives as an object with global, setInterval, setTimeout functions etc. The reason behind getting an empty object initially is the way Node.js execution process works. Now this is a topic for another day, how exactly Node.js code flow works; let us just get to know what data we get from using this keyword directly on root level:
console.log('Root Level:', this);function breath() {
console.log('Root Level Inside Function:', this);
}breath();/*
Root Level: {}
Root Level Inside Function: Object [global] {
global: [Circular *1],
clearInterval: [Function: clearInterval],
clearTimeout: [Function: clearTimeout],
setInterval: [Function: setInterval],
setTimeout: [Function: setTimeout] {
[Symbol(nodejs.util.promisify.custom)]: [Getter]
},
queueMicrotask: [Function: queueMicrotask],
performance: [Getter/Setter],
clearImmediate: [Function: clearImmediate],
setImmediate: [Function: setImmediate] {
[Symbol(nodejs.util.promisify.custom)]: [Getter]
}
}
*/
So we can simply conclude that at the root level of any JS file this keyword returns the global Window object and in case of Node.js, as per the execution we will get an empty object for this initially but later you can get the root Node.js export object mostly in any function (and not always; because it depends on how the function is invoked).
6. this with Strict Mode
We behave well when our moms get strict sometimes, and nothing different for this keyword. When we use this keyword inside any root level function it will casually return the global Window object. But when you use the strict mode it returns undefined (lol, same as we do when mom strictly asks about who ate the chocolate?).
Checkout below functions one is damnStrict() and the other one is prettyCasual() and the results are in front of you.
function damnStrict() {
'use strict'
console.log('Damn Strict:', this);
}function prettyCasual() {
console.log('Pretty Casual:', this);
}damnStrict();
prettyCasual();/*
Damn Strict: undefined
Pretty Casual: Window {window: Window, self: Window, document: document, name: "", location: Location, …}
*/
So we can simply conclude that in strict mode we will get undefined not any global object from a function. Note this goes for Node.js too, in case of strict mode the previous example’s breath() functions will also return undefined.
7. this with Arrow Functions
Lastly, but worth mentioning is this keyword with fan favorite arrow function. This keyword inside an arrow function refers to an instance of immediate scope. At root level the scope of any function points to the global object, so it will simply return the global Window object. Checkout the below code:
const openArrow = () => {
console.log('Arrow Function:', this);
};openArrow();// Arrow Function: Window {window: Window, self: Window, document: document, name: "", location: Location, …}
Note that the strict mode won’t change this keyword’s behaviour in an arrow function, the rules will remain the same, it will always refer to an instance of immediate scrope. Checkout the below code:
const openArrow = () => {
'use strict'
console.log('Arrow Function:', this);
};openArrow();// Arrow Function: Window {window: Window, self: Window, document: document, name: "", location: Location, …}
Now let us checkout this keyword with the object’s arrow method. Checkout below code:
const arrowGuy1 = {
arrowCount: 5,
shootArrow: () => {
console.log('Guy 1, Arrow Count:', this.arrowCount);
}
};arrowGuy1.shootArrow();// Guy 1, Arrow Count: undefined
As mentioned the this keyword inside an arrow function will refer to scope instance and not to object’s instance, so in the above case the this keyword is pointing to global Window object and not to arrowGuy1 that is why we get undefined since arrowCount property is not available in Window object. But if we have something like below:
const arrowGuy2 = {
arrowCount: 3,
shootArrow: function () {
setTimeout(() => {
console.log('Guy 2, Arrow Count:', this.arrowCount);
}, 500);
}
};arrowGuy2.shootArrow();// Guy 2, Arrow Count: 3
In the above code we simply wrap out the previous shootArrow() function inside setTimeout() function. Since the shootArrow() function is now a normal function and not an arrow function it will get the current object instance and the scope of setTimeout() this keyword points to arrowGuy2 object we can obtain the arrowCount inside the setTimeout() function. And hence we can get count 3 for guy 2.
So we can simply conclude that this keyword inside an arrow function works as per inclosing scopes and doesn’t bother about the strict mode.
Man! There are many rules for this keyword, quite confusing sometimes but worth remembering, as these rules will be the only light in the darkest night. So whenever you find this keyword just recall these rules and try to identify which scenario right now your this keyword is residing in (some crazy chess rules).
We can even rephrase the Bruce Lee’s legendary quote for this keyword:
Be like this keyword making its way through cracks. Do not be assertive, but adjust to the object, and you shall find a way around or through it. If nothing within you stays rigid, outward things will disclose themselves.
Empty your mind, be formless. Shapeless, like this keyword. If you put this keyword into an object, it becomes the object. You put this keyword into a class and it becomes the class object. You put it in an element’s event listener, it becomes the element. Now, this keyword can flow or it can crash. Be this keyword, my friend.
- Bruce Lee (for this keyword)
Summary
There are tons of articles out there explaining this topic; and this is just my take on this keyword, hope it helps to understand. To understand this keyword further one should code and test by themself and check every possible scenario they can explore.
Hope this article helps.
Originally published at https://codeomelet.com.