How not to be afraid of the 'this' keyword
To start talking about the 'this' keyword, we should first understand where it comes from. When a function (or methods) gets invoked, it has 2 phases: the creation phase, and the execution phase.
There are a lot of things to discuss about when it comes to those phases, and a lot of concepts like execution context, lexical environment, variable environment, scope and scope chain (But don't worry, I will discuss them in depths in the next article). Therefore, for simplicity, in this article we only need to know that the value of 'this' keyword is NOT static.
It depends on how the function is called, and its value is only assigned when the function is actually called.
What do I mean by "It depends on how the function is called"? glad you asked! In JavaScript, there are different ways in which functions can be called and as a result the 'this' keyword gets different value:
1. Simple function call:
In this case, the 'this' keyword points to the global object - the window, but in 'strict mode' the 'this' keyword will be undefined.
'use strict'console.log(this); // window// es5 way for writing functionconst calcAgeES5 = function(birthYear) { console.log(new Date().getFullYear() - birthYear); console.log(this); // undefined (without the 'use strict' - window)}calcAgeES5(1991);// es6 way for writing functionconst calcAgeES6 = birthYear => { console.log(new Date().getFullYear() - birthYear); console.log(this); // window}calcAgeES6(1991);
2. Method:
Method is a function attached to an object. In this case the 'this' keyword points to the object on which the method is called, or in other words, it points to the object that is calling the method.
const marry = { birthYear: 1988, calcAge: function() { console.log(this) // marry return new Date().getFullYear() - this.birthYear; }}marry.calcAge();const joe = { birthYear: 2010}joe.calcAge = marry.calcAge;joe.calcAge(); // this => joe
In the following example we save the 'calcAge' method called on 'marry' to a variable called 'func'. When we will log 'func' we will see the method printed to the console: ƒ () { return new Date().getFullYear() - this.birthYear; console.log(this); }
But, on a strict mode, when we will call func(), the 'this' keyword of that execution context will be undefined because it's a regular function call which is not attached to any object. Without the 'use strict' the 'this' keyword will be the window object.
'use strict'const func = marry.calcAge;console.log(func) // log the functionfunc(); // this => undefined
3. Arrow functions:
Arrow functions do not get their own 'this' keyword, they get the 'this' keyword of the surrounded function (the parent function). It's called the lexical 'this' keyword because it simply gets picked up from the outer lexical scope. In the following example the parent scope is the global scope because the 'marry' object lives in the global scope, therefore the 'this' keyword is the window.
const marry = { firstName: 'Marry', birthYear: 1988, calcAge: function() { console.log(this) // marry return new Date().getFullYear() - this.birthYear; }, greet: () => { console.log(this); // window console.log(`Hello ${this.firstName}`); }}marry.greet(); // Hey undefined
NOTE: variables declared with 'var' create properties on the global object, therefore in this case where we declared firstName with 'var' and we will call 'marry.greet()', we will get 'Hello Tomas'. It happens because when the greet method gets called it searches for firstName variable in its local scope, does not find it and go up in the scope chain untill it find it on the window object, there we have window.firstName because of the decleration with 'var'.
var firstName = 'Tomas';marry.greet(); // Hello Tomas
The way to fix the problem with the 'greet' method is to write it in a form of regular function and not arrow function.
const marry = { firstName: 'Marry', birthYear: 1988, calcAge: function() { console.log(this) // marry - the object that call the method return new Date().getFullYear() - this.birthYear; }, greet: function() { console.log(this); // marry console.log(`Hello ${this.firstName}`); // Hello Marry }}marry.greet();
Function inside a method:
const marry = { birthYear: 1988, calcAge: function() { const isMillenial = function() { console.log(this.birthYear >= 1981 && this.birthYear <= 1996); // undefined } isMillenial(); return new Date().getFullYear() - this.birthYear; }}marry.calcAge();
'isMillenial' is a regular function call even thought it happens inside of a method, and as we have learned earlier in this article inside a regular function call the 'this' keyword is the global object - window (and undefined in 'use strict' mode). There are 2 solutions for the "problem":
- Outside the 'isMillenial' function save the 'this' to a variable:
const self = this; // self or thatconst isMillenial = function() { console.log(self.birthYear >= 1981 && self.birthYear <= 1996); // true}isMillenial();
- Use arrow function which takes the 'this' of his surrounded environment, which in this case is 'calcAge' method, which its 'this' keyword is 'marry' object
const marry = { birthYear: 1988, calcAge: function() { const isMillenial = () => { console.log(this.birthYear >= 1981 && this.birthYear <= 1996); // true } isMillenial(); return 2020 - this.year; }}marry.calcAge();
4. The 'new' operator
To explain about the new operator we first need to understand what is a constructor function. A constructor function is a function that used as a blueprint to create objects, therefore when we call the function, it must be with the new operator and when we write a constructor function, the name should start with a capital letter.
Constructor functions are used to stimulate classes which we have now in ES6 but as a syntactic sugar. Arrow function cannot be a function constructor because as I have stated, it does not have its own 'this' keyword. Only functions declaration and functions expression can be a constructor function.
const Person = function(firstName, birthYear) { console.log(this); // Person {} this.firstName = firstName; this.birthYear = birthYear; // NEVER DO THIS this.calcAge = function() { console.log(2020 - this.birthYear); }}const amit = new Person('Amit', 1991);console.log(amit); // Person {firstName: "Amit", birthYear: 1991}
When we call a constructor function with the new operator there are 4 steps that happen behind the scenes
- new empty object is created
- the function is called and 'this' keyword point on the newly created object;
- the newly created object has link to the prototype (on our example: Person )
- the new object created on step 1 returned from the constructor function.
NOTE: you should never create a method inside a constructor function, because if that function has many methods, each object that build based on it, would carry around all the methods. Instead, we should use prototype inheritance, but this is a topic for another article.
5. call, apply, bind
Help us to set the 'this' keyword manually
const lufthansa = { airline: 'Lufthansa', iataCode: 'LH', bookings: [], book(flightNum, name) { console.log(`${name} booked a seat on ${this.airline} flight ${this.iataCode}${flightNum}`); this.bookings.push({ flight: `${this.iataCode}${flightNum}`, passengerName: name }) }}lufthansa.book(239, 'John Lennon');lufthansa.book(447, 'Amy Winehouse');
Now, let's say we have another airline, with different properties but it still needs the book method. We can copy and paste that method, but it's bad practice. What we should do is to store the method in a variable, so we can use it in other places. The reason why we can do it like that is because in js functions are first class citizen.
const book = lufthansa.book();book(123, 'Marge Simpson'); // Cannot read property airline of undefined
Because book is a regular function call, the 'this' keyword points to undefined (in strict mode).
The way to fix it is to tell JS explicitly what the 'this' keyword should be and here come call, apply and bind.
- call && apply: functions that their first argument is the one we want the 'this' keyword to point on. The other arguments are the argument which the function we call on the call or apply methods takes. The difference between call and apply is that apply gets the argument as an array (or an array-like object) and call gets them individually.
const elal = { airline: 'Elal', iataCode: 'EL', bookings: []}book.call(elal, 123, 'Marge Simpson'); // 'Marje Simpson' books a seat on Elal flight EL123book.apply(elal, [789, 'Alice Cooper']); // 'Alice Cooper' books a seat on Elal flight EL789
- bind: also allows us to manually set the 'this' keyword for any function call. The difference is that bind does not immediately call the function, instead it returns a new function where the 'this' keyword set to the provided value.
const bookEl = book.bind(elal);bookEl(123, 'Marge Simpson') // 'Marje Simpson' books a seat on Elal flight EL123// OR we can provide arguments (partial application)const bookEl123 = book.bind(elal, 123);bookEl123('Marge Simpson') // 'Marje Simpson' books a seat on Elal flight EL123bookEl123('Diana Ross') // 'Dianna Rose' books a seat on Elal flight EL123
There are cases when we do not mind what the 'this' keyword is, but we still use bind, for example in partial application when we preset parameters. Be aware that the argument you want to preset must be the first argument;
const addTax = (rate, value) => value + value * rate;const addTax30 = addTax(null, 0.3);addTax30(200);
6. Event listener:
In an event handler function the 'this' keyword always points on the DOM element that the handler function is attached to.
<button class="buy">Buy a new plane</button>
const lufthansa = { airline: 'Lufthansa', iataCode: 'LH', bookings: []}lufthansa.planes = 300;lufthansa.byPlane = function() { console.log(this); // <button class="buy">Buy a new plane</button> this.planes++; console.log(this.planes); // NaN}document.querySelector('.buy').addEventListener('click', lufthansa.byPlane);
If we will run this code, the line we log 'this' to the console will give us the reference to the DOM element the handler function is attached to, therefore at the line we log lufthansa's planes to the console we will get NaN.
The way to fix it is to use the bind method because in event listener we don't want to immediately call the function, we just pass a reference to the function which will be call when the event accrues.
document.querySelector('.buy').addEventListener('click', lufthansa.byPlane.bind(lufthansa));
Conclusions:
The 'this' keyword isn't static. It depends on how the function is called, and its value is only assign when the function is called.
In this article we have covered many cases when the 'this' keyword gets different values, so to summarize the article I am going to tell you what the 'this' keyword will never be:
- 'this' will never point to the function in which we are using it
- 'this' will never point to the variable environment of the function
As a side note, I would like to share with you one of the reasons I decided to write the first blog post about the 'this' keyword. When I started learning JavaScript and going to my first interviews I was asked a lot about the 'this' keyword, and even I went through that topic before every interview, when the interviewer asked me a question about the 'this' keyword, I got confused and nervous and did not get it right.
Thanks for reading, hope you enjoyed and learned something new or at least feel more comfortable with the 'this' keyword now.
← Go to All Posts