🍉

Understanding this and losing context

February 24, 2023

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.

  1. Default or implicit behavior
  2. Invoke the function as an object method
  3. Set this explicitly by call, bind and apply
  4. 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

2022 — 2023 Made by Arbuznik with ♡️ and Gatsby