Introduction

This article discusses specifics of using thiskeyword and changing contexts in regular and arrow functions. By the end of this article you will know how to use thiskeyword properly, important difference between regular and arrow functions and related features of the strict mode.

Requirements: basic JS knowledge.

Methods And 'this' Keyword

A method is a function that is stored as a property of an object. Context of a function is an object inside which this function is defined. In methods we would often want to have an access to properties of the context. It can be achieved usingthiskeyword. For example:

const APIService = {
  data: "",
  fetch: function() {
    // Note, how 'this' refers to the APIService object
    this.data = "Improtant data"
  } 
}

The Global Object As A Context

The global object is a special object, that is defined by an interpreter. The global object provides us with:

  1. Global constants (undefined, Infinity, NaN)
  2. Global functions (isNan(), parseInt())
  3. Constructor functions (Date(), RegExp(), String(), Object())
  4. Global objects (Math, JSON)

In browser environment, the global object iswindow, and in Node it isglobal.

Now, let's consider the following function:

secret = "42";  // Note, it is defined as a property, without let or const

function showSecret() {
   console.log(this.secret);  // Where does 'this' refer now?
};

That's where it gets a little bit trickier. One could argue, that function above is not a method (since it is not a property of an object), thus this should just be undefined. On the other side, we could say that the function is a method of the global object. Both opinions are valid. This is why the code above will behave differently depending on whether the strict mode is used or not.

If strict mode is disabled, this will refer to the global object and 42 will be printed. If strict mode is enabled, this will be undefined, and thus we will have a TypeError. Note, that in order for it to work, secret is defined without let or const , so it becomes a property of the global object. In fact, strict mode won't even allow you to redefine properties of the global object.

// No strict mode

secret = "42";  // defining The Global Object property

function showSecret() {
   console.log(this.secret);  // 'this' refers to global object
};

showSecret();  // 42 is printed
// Enabling strict mode
"use strict"

secret = "42";  // Will throw Reference Error, not allowed to modify the global object

function showSecret() {
   console.log(this.secret);  // will throw TypeError, since 'this' is undefined
};

showSecret();  // Error

Context Of A Function Inside A Method

Let's take a look at this example now:

const APIService = {
  data: "",
  counter: 0  // Keeps track of number of calls to api
  fetch: function() {
    // Define helper function to increment counter
    function incrementCounter() {
      this.counter += 1;  // Were 'this' refers to?
    }  

    data = "Important Data" // Getting data from API...  
    incrementCounter();  // Call helper function 
  }
};

The code above is incorrect. this keyword doesn't scope the way variables do. Function incrementCounter is not defined inside an object, so the same logic as in the previous chapter applies. If strict mode is enabled, this inside incrementCounterwill be undefined, otherwise it will refer to the global object. In any case, code above won't work as intendent.

Context Of Arrow Functions

Not like regular function, arrow functions, presented in ES6, do not create own this reference, and just inherit it from an outer scope. So, if we take the previous example, and use arrow function instead, it will work as intender:

const APIService = {
  data: "",
  counter: 0  // Keeps track of number of calls to api
  fetch: function() {
    // Use arrow function
    const incrementCounter = () => {
      // Now `this` referes to the same object as fetch method does
      //    i.e. APIService object
      this.counter += 1;  
    }  

    data = "Important Data" // Getting data from API...  
    incrementCounter();  // Call helper function 
  }
};

Arrow functions provide us simpler way of defining such helper functions and allows us to be more flexible with closures.

Context Binding

It is possible to change context of a regular function using methods call, apply, bind. Note, that context of an arrow function can not be changed, since this keyword is inherited from outer scope at a place where that arrow function was defined.

call method allows us to call function once, using provided object as a context. apply method does the same thing, but accepts arguments for the called function in a different format (as a list). Example:

function getLengthByFactor(factor) {
  return factor * Math.sqrt(this.x * this.x + this.y * this.y);
}

const vector1 = {
  x: 2,
  y: 0
}

const vector2 = {
  x: 4,
  y: -3
}

// Invoke function with vector1 as a context and 2 as argument
getLengthByFactor.call(vector1, 2);  // return 4

// Invoke function with vector2 as a context and 2 as argument
getLengthByFactor.apply(vector2, [2]); // return 10

Note, that call and apply do not change context of a function permanently. If you want to change context of a function indefinitely, you should use bind method, which doesn't change a context of original function, but returns the same function with provided context. Example:

function getLengthByFactor(factor) {
  return factor * Math.sqrt(this.x * this.x + this.y * this.y);
}

const vector1 = {
  x: 2,
  y: 0
}

const vector2 = {
  x: 4,
  y: -3
}

const getLengthByFactorVector1 = getLengthByFactor.bind(vector1);
getLengthByFactorVector1(); // returns 4

Although call apply and bind is useless for arrow functions in terms of context change, but it can be used to implement currying:

const sum = (x, y) => x + y;

// Bind context object to null and first argument to 5
const sumWithFive = sum.bind(null, 5);

// Now we can call sumWithFive as sum where x is substituted with 5
sumWithFive(3); // returns 8 

Summary

In modern ES6 such manipulations of a context are not seen much, but they are vital for implementing OOP in JS under the hood. I, as a person who started programming journey with C and C++, for a long time had a wrong intuition behind OOP in JS. For example, mechanisms like inheritance in JS is implemented using Object.prototype, and contexts and this keyword are vital in that case. Yes, modern ES6 provides us with class keyword, and we do not have to think about all that context changing. Yet, I believe it is important to have an intuition about underlying mechanisms to use the tool right.