Understanding this and losing context
General rule
This
in JavaScript is a reference to an object, and it can vary. This
depends on where and when a function was invoked. It doesn’t matter where the function is originally declared. This
is redefined on every invocation. So in order to understand what this
refers to, we need to explore the place where the function is invoked.
// 'this' inside a global scope of an empty node.js project
console.log(this); // {}
// 'this' inside a global scope in a browser
console.log(this); // Window global object
Strict mode
Called inside a function declared in a global scope this
refers to Window
.
function hi() {
console.log(this);
}
hi(); // Window global object
With strict mode.
function hi() {
"use strict"; // strict mode on
console.log(this);
}
hi(); // undefined
You can say that strict mode forbids this
to ‘float’ outside of the function’s scope.
Losing context
When talking about the context and losing the context, by context we mean this
. When we lose the context, we lose the reference to this
.
Example: eventListener inside a class
Check the following code.
// losing context
class Button {
constructor() {
this.buttonName = "Pretty button";
}
onClick() {
console.log(this); // <div class="menu-button" />
console.log(this.buttonName); // undefined
}
setEventListener() {
// set onClick method as callback
document
.querySelector(".menu-button")
.addEventListener("click", this.onClick);
}
}
const button = new Button();
button.setEventListener();
When we click on the menu button, we hope to get “Pretty button” printed out in the console. But instead, we get undefined
. Because we passed function as a callback inside event listener, inside this
is an element with selector “.menu-button”. And it doesn’t have buttonName
property.
this
inside event listener callback refers to DOM element
Example: object method
// 'use strict'
const dog = {
nickname: 'Barky',
bark: function () {
console.log(`${this.nickname} barked`);
}
}
const neighborsDog = dog.bark;
neighborsDog(); // undefined barked
Should’ve worked, right? But check the invocation point of neighborsDog()
. Since it’s called in global scope, this
refers to a Window
, and Window
doesn’t have the nickname
property. If we turn on the strict mode, our app will crash with a TypeError
.
Example: passing function as a handler
const button = {
buttonName: 'Pretty button',
onClick: function () {
console.log(this.buttonName);
}
};
const btn = document.querySelector('.logo');
btn.addEventListener('click', button.onClick);
When the click happens, this
equals the DOM element with logo
selector. It doesn’t have a buttonName
property, so we get undefined
. It happens because we don’t invoke the function. We pass it, separately from the object, to be later invoked on a button element.
We can fix it by passing a callback, and inside the callback, we will invoke the function with object “attached” to it before the dot, as this
.
const button = {
buttonName: 'Pretty button',
onClick: function () {
console.log(this.buttonName);
}
};
const btn = document.querySelector('.logo');
btn.addEventListener('click', function () {
button.onClick();
});
How to set this
explicitly
There are 4 ways to set this
, depending on where it is invoked, sorted in order of increasing priority.
- Default or implicit behavior
- Invoke the function as an object method
- Set
this
explicitly bycall
,bind
andapply
- Invoke the function with
new
Another way to set this
is to use arrow functions.
Method of an object
// 'this' inside an object refers to an object
const obj = {
prop: "property",
method: function () {
console.log(this.prop);
},
};
obj.method(); // property
We can change the point of the function’s invocation. Now we are calling it not as a global function, but as a method of an object Window
, this way setting a context.
window.prop = 'Property'
function functionInGlobalScope() {
'use strict';
console.log(this.prop);
}
window.functionInGlobalScope(); // 'Property'
It’s easy to find out what is the context in this case. The part before the dot is this
.
Call & Apply
Call
allows as to invoke a function with provided this
.
First argument is this
. All the following arguments will be passed into the intial function as parameters.
function fixVehicle(wash) {
console.log(`Fixing: ${this.color} ${this.model}`);
console.log(`Wash after repairs? ${wash ? 'yes' : 'no'}`)
}
fixVehicle(); // Fixing: undefined undefined
const myCar = {
model: "Tesla",
color: "red",
};
fixVehicle.call(myCar, wash = true); // Fixing: red Tesla
Apply
does the same. The only difference is how the rest of the arguments are passed into initial function. Apply
uses an array of arguments. Keep in mind though, that in the receiving function we don’t use array destructurization. We write the arguments as usual, comma separated.
function fixVehicle(wash) {
console.log(`Fixing: ${this.color} ${this.model}`);
console.log(`Wash after repairs? ${wash ? 'yes' : 'no'}`)
}
fixVehicle(); // Fixing: undefined undefined
const myCar = {
model: "Tesla",
color: "red",
};
fixVehicle.apply(myCar, [wash = true]); // Fixing: red Tesla
Bind
Bind creates another function, a wrapper around the initial function. We explicitly say that our function is the initial function + this. And you can no longer change the resulting wrapper’s this
.
function fixVehicle() {
console.log(`Fixing: ${this.color} ${this.model}`);
}
fixVehicle(); // Fixing: undefined undefined
const myCar = {
model: "Tesla",
color: "red",
};
const fixMyCar = fixVehicle.bind(myCar); // creating new function
fixMyCar(); // Fixing: red Tesla
Another example.
class Button {
constructor() {
this.buttonName = "Pretty button";
}
onClick() {
console.log(this.buttonName);
}
setEventListener() {
// set onClick method as callback
document
.querySelector(".Header-logo")
.addEventListener("click", this.onClick.bind(this)); // this is now this class, and it has a buttonName property
}
}
const button = new Button();
button.setEventListener();
Now if we click on the button we get ‘Pretty button’ in the console.
Bind doesn’t call your function. It creates a wrapper (new function) of function + this.
Invoke the function with new
new
invokes the function like a constructor. When we do this, under the hood JavaScript creates this
object for us and returns it. Functions invoked as new
always return this
object, even if the original function doesn’t have a return value at all.
function Car (seats, wheels) {
// this = {}; // implicitly
this.seats = seats;
this.wheels = wheels;
// return this; // implicitly
}
const vehicle = new Car (2, 4);
console.log(vehicle) // Car {seats: 2, wheels: 4}
There’s always an exception though. If the initial function already returns an object, then if invoked with new
it still will return an initial object.
We cannot call
, bind
or apply
while invoking function with new
. But, we can set the context in the object that we have created.
function Car (name) {
this.name = 'Tesla';
this.logName = function () {
console.log(this.name);
}
}
const vehicle = new Car ();
const Renault = {
name: 'Renault'
}
vehicle.logName() // Tesla
vehicle.logName.call(Renault) // Renault
vehicle.logName.apply(Renault) // Renault
vehicle.logName.bind(Renault)() // Renault
Arrow functions
There is one exception when this
doesn’t depend on where the function was invoked. This
in arrow functions is referenced in the moment of function declaration and stored forever.
Arrow functions don’t have their own this
, it’s taken from the closures. That’s why it’s useless to use arrow functions as object methods. Because this
will be taken from the global scope.
// 'this' inside an arrow function in an object
const obj = {
prop: "property",
method: () => {
console.log(this);
console.log(this.prop);
},
};
obj.method();
// Window
// undefined
Call, Apply & Bind with Arrow functions
Since this
of an arrow function is set in the moment of it’s declaration, Call
, Apply
and Bind
won’t have any effect.
function Car (name) {
this.name = 'Tesla';
this.logName = () => {
console.log(this.name);
}
}
const vehicle = new Car ();
const Renault = {
name: 'Renault'
}
vehicle.logName() // Tesla
vehicle.logName.call(Renault) // Tesla
vehicle.logName.apply(Renault) // Tesla
vehicle.logName.bind(Renault)() // Tesla
new
with arrow functions
Constructor function references to this
an object, created under the hood. Since an arrow function doesn’t have this
, it won’t work.
const Car = (name) => {
this.name = 'Tesla';
}
const vehicle = new Car (); // TypeError: Car is not a constructor
Arrow functions in prototypes
If you want to use methods in prototypes, don’t use arrow functions. In the moment of declaration, this
will always be a global scope Window
.
function Car() {
this.carName = 'Tesla';
}
Car.prototype.printName = () => {
console.log(`Name: ${this.carName}`)
};
const car = new Car();
car.printName()
Comments