Understanding The  "this" Parameter In JavaScript Once And For All

Understanding The "this" Parameter In JavaScript Once And For All

The this parameter in JavaScript is like that oddly concealed obstacle on a path that keeps tripping people. For the JavaScript beginner, it is often a thing of much unclarity, while for many a fairly-seasoned developer, it is one of those things they've figured out how to use but have never truly understood.

The this parameter is a vital ingredient in object-oriented JavaScript and a deep understanding of how it behaves is key to unlocking so many other concepts. By the end of this article, you should have gotten the insight needed to forever dispel any uncertainty you may have on this subject matter (pun so intended).


What exactly is "this"?

When a function is invoked, in addition to the explicit arguments passed to it, it receives other implicit parameters under the hood that are accessible within the body of the function. One of these is the this parameter which represents the object that is associated with the function invocation. It is often referred to as the function context.

However, this and the way it is determined is one of those things in JavaScript that are not so straightforward. The value of this is not only dictated by how and where a function is defined, but also, largely by how it is invoked. To begin making heads or tails of how this in a function is determined, we have to revisit our knowledge of function behavior.


Invoking functions

There are four ways functions can be invoked in JavaScript, each with its own peculiarity:

  1. As a function—averageJoe(), in which the function is invoked in a straightforward manner
  2. As a method—averageJoe.talk(), which ties the invocation to an object, enabling object-oriented programming
  3. As a constructor—new AverageJoe(), in which a new object is brought into being
  4. Via the function’s apply or call methods—averageJoe.call(someObject) or averageJoe.apply(someObject)

— Secrets of the JavaScript Ninja, Second Edition

As a function

When a function is invoked in a straightforward manner, its function context (that is, the this value) can be two things. In non-strict mode, the global context (the window object) becomes the function context. In strict mode, it will be undefined.

function averageJoe() {
    console.log(this)
}

function strictJoe() {
    "use strict"
    console.log(this)
}

averageJoe()  //  window object
strictJoe()  //  undefined

The outcome is the same even if the function is defined within a function, so long it is invoked in a straightforward manner:

function outer() {
  function inner() {
    console.log(this)
  }

  function strictInner() {
    "use strict"
    console.log(this)
  }

  inner()  //  window object
  strictInner()  //  undefined
}
outer()

As a method

Functions can also be properties in objects and in that capacity, they are known as methods. When a function is invoked through an object (as a method), the object itself becomes the function context and is accessible within the body of the method via the this parameter.

averageJoe = {
  name: "Joe",
  talk: function() {
    console.log(this)
  }
}

averageJoe.talk()  //  {name: "Joe", talk: ƒ}

As a constructor

Constructor functions are essentially the same old, run-of-the-mill functions we've been dealing with. As the name implies, we use them to "construct" new objects. To invoke a function as a constructor, we precede the function invocation with the new keyword.

When a function is invoked with the new keyword, a new object instance is created and provided to the function as its context. Within the function, any reference to this is a reference to the newly created object instance.

function AverageJoe() {
  console.log(this)  //  {} 'new object'
  this.name = "Joe"
  console.log(this)  //  {name: "Joe"}
}
new AverageJoe()

Via the apply or call method

Functions in JavaScript have access to two inbuilt methods: apply and call.

func.apply(thisArg, argArray)

func.call(thisArg, arg1, arg2, ...)

They allow us invoke a function and explicitly tie it to an object. Any object supplied to the thisArg parameter becomes the function context and what is referenced by this within the function.

let averageJoe = {
  name: "Joe"
}

function randomGuy() {
  console.log(this)
}

randomGuy.call(averageJoe)  //  {name: "Joe"}


Taking a closer look at functions

Functions being first-class objects in JavaScript mean they can, among other things, be assigned to things and passed around just like other value types. Of course, being objects, when we do assign or pass them around, what we are actually passing is their reference.

This flexibility around functions creates plentiful variety in the manner in which they are applied and used. Let's see how the concepts we've covered so far come into play in some of these scenarios.

function loneGuy() {
  console.log(this)
}
loneGuy()  //  window object


let averageJoe = {
  name: "Joe",
  talk: loneGuy
}
averageJoe.talk()  //   {name: "Joe", talk: ƒ}


let anotherAverageJoe = {
  name: "Another Joe",
  speak: averageJoe.talk
}
anotherAverageJoe.speak()  //  {name: "Another Joe", speak: ƒ}


let anotherLoneGuy = anotherAverageJoe.speak
anotherLoneGuy()  //  window object


anotherLoneGuy.apply(averageJoe)  //  {name: "Joe", talk: ƒ}
averageJoe.talk.call(anotherAverageJoe)  //  {name: "Another Joe", speak: ƒ}


We begin by defining a function loneGuy that logs the current value of this within its function body:

function loneGuy() {
  console.log(this)
}

When invoked as an ordinary, standalone function, the window object is outputted as the value of this:

loneGuy()  //  window object

We go on to create an object that has a talk method that references the loneGuy function. When the talk method is invoked, its parent object, averageJoe now becomes the this value:

let averageJoe = {
  name: "Joe",
  talk: loneGuy
}
averageJoe.talk()  //   {name: "Joe", talk: ƒ}

We create another object anotherAverageJoe whose speak method is a reference to averageJoe.talk. The speak method is invoked via its parent object anotherAverageJoe, which is rightly outputted as the this value.

let anotherAverageJoe = {
  name: "Another Joe",
  speak: averageJoe.talk
}
anotherAverageJoe.speak()  //  {name: "Another Joe", speak: ƒ}

We create a new variable anotherLoneGuy and pass it a reference to anotherAverageJoe.speak. We go ahead to invoke it in a straightforward manner and sure enough, it gets the window object as its this value.

let anotherLoneGuy = anotherAverageJoe.speak
anotherLoneGuy()  //  window object

Next, we invoke the newly created anotherLoneGuy via the built-in apply method and explicitly provide averageJoe as its function context. Expectedly, it runs and logs averageJoe as its this value. We also invoke averageJoe.talk via the call method and provide anotherAverageJoe as its function context which it duly outputs as its this value, despite being a method in averageJoe.

anotherLoneGuy.apply(averageJoe)  //  {name: "Joe", talk: ƒ}
averageJoe.talk.call(anotherAverageJoe)  //  {name: "Another Joe", speak: ƒ}

From all the passing around and reassigning of our initial function, we can see that whilst where and how a function is defined may have a hand in how its this value is arrived at, how it eventually gets invoked is the most determining factor.


The curious case of arrow functions

Arrow functions came with ES6 and brought new elegance to how functions were wielded in JavaScript. They discarded some of the syntactic baggage of traditional functions and allowed functions to be expressed more succinctly and lucidly.

Arrow function expressions weren't just a syntactic retailoring of traditional functions though. They differed not only in syntax but slightly in behavior as well, one of which being how function context is determined. Arrow functions don’t have their own this value. Instead, they remember the value of the this parameter at the time of their definition.

Let's understand this by walking through some code.

function randomGuy() {
  function regularFunc() {
    console.log(this)
  }

  const arrowFunc = () => {
    console.log(this)
  }

  regularFunc()
  arrowFunc()
}
randomGuy()
//  regularFunc –> window object
//  arrowFunc –> window object


let averageJoe = {
  name: "Joe",
  talk: randomGuy
}
averageJoe.talk()
//  regularFunc –> window object
//  arrowFunc –> {name: "Joe", talk: ƒ}


To begin, we define a randomGuy function, inside of which we house two other functions—a normal function regularFunc and an arrow function expression arrowFunc—both of which log the value of this inside their respective bodies.

function randomGuy() {
  function regularFunc() {
    console.log(this)
  }

  const arrowFunc = () => {
    console.log(this)
  }

  regularFunc()
  arrowFunc()
}

We invoke randomGuy the straightforward way and its function context becomes the window object. The code executes beyond the function definitions and reaches the regularFunc invocation. It is also invoked in a straightforward fashion, thus, it gets the window object as its function context as well. Next, arrowFunc is invoked and as an arrow function that doesn't determine its own this value, it takes on the this value existing at the time it was defined, which was the window object.

randomGuy()
//  regularFunc –> window object
//  arrowFunc –> window object

It is vital to understand the nuance here. Even though both functions ended up with the window object as their respective this values, the reasons were different. For regularFunc it was because it was invoked in straightforward manner, while for arrowFunc it was because the window object was the existing this value at the time of its definition and that was what it stuck with.


We go on to define an object averageJoe which has a talk method that is a reference to randomGuy.

let averageJoe = {
  name: "Joe",
  talk: randomGuy
}

When the talk method is invoked, the this value within its body becomes its parent object averageJoe through which it was invoked. We go past the function definitions and once again, regularFunc gets invoked in straightforward fashion making the window object its this value. Next, arrowFunc is invoked, and being an arrow function, it remembers the this value that existed at the time it was defined (the averageJoe object) and inherits it as its own this value.

averageJoe.talk()
//  regularFunc –> window object
//  arrowFunc –> {name: "Joe", talk: ƒ}

Arrow functions get their function context from the existing function context at the time of their definition. They remember this context and stick faithfully to it no matter how they're invoked later on.

Arrow functions as callbacks

An area where the practicality of arrow functions comes to bear is in the use of callback functions. Traditional functions have always been quirky in this regard and prior to arrow functions, developers had to resort to workarounds when using them as callbacks in certain cases. Take a look at this code for example:

let averageJoe = {
  hobbies: ["reading", "coding", "blogging"],
  printHobby: function(hobby) {
    console.log(hobby)
  },
  printHobbies: function() {
    this.hobbies.forEach(function(hobby) {
      this.printHobby(hobby)
    })
  }
}
averageJoe.printHobbies()  //  Uncaught TypeError: this.printHobby is not a function

In this scenario, using a traditional function expression as our callback brought us nothing but heartbreak. When the anonymous callback function we passed to the forEach method gets invoked, it takes on the window object as its function context and which, of course, isn't where the printHobby method resides. Hence, the error we got.

However, when we make use of an arrow function instead, we see that it captures the prevailing this value at the time of its definition (the averageJoe object) and this, in turn, leads to the output we desire:

// ...

  printHobbies: function() {
    this.hobbies.forEach(hobby => {
      this.printHobby(hobby)
    })
  }
}
averageJoe.printHobbies()  //  "reading", "coding", "blogging"

Using arrow functions as methods

You should be a bit careful with arrow functions. Their characteristic of inheriting the this parameter leads to a quirk when we use them as methods.

Take this for example:

let averageJoe = {
  name: "Joe",
  talk: () => {
    console.log(this)
   }
}

averageJoe.talk()  //  window object

Within the talk method, this now references the window object as opposed to its parent object averageJoe. Crazy, right? Don't panic, I will explain.

It's actually quite simple. Arrow functions always sticking to the this value existing at the time of their definition means that they won't take their parent objects as their function context when they're invoked through them as methods. In this particular case, averageJoe is created in global JavaScript code (that is, not within a function) where the value of this is the window object and that is what the arrow function expression used for the talk method stuck with.

This same behavior of arrow functions leads to a different outcome when we come to constructor functions:

function AverageJoe() {
  this.name = "Joe"
  this.talk = () => {
      console.log(this)
    }
}

let averageJoe = new AverageJoe()
averageJoe.talk()  //  {name: "Joe", talk: ƒ}

As we know, invoking constructor functions with the new keyword leads to a new object instance being created and made the function context. So, in essence, the object that we are passing the arrow function to as a method is also the existing function context at the time it was defined and that is what it will stick to whenever it is invoked.

In fact, however it is invoked.

averageJoe.talk()  //  {name: "Joe", talk: ƒ}

let loneGuy = averageJoe.talk
loneGuy()  //  {name: "Joe", talk: ƒ}  not window object

averageJoe.talk.call(window)  //  {name: "Joe", talk: ƒ}  still not window object

let anotherLoneGuy = averageJoe.talk.bind(window)
anotherLoneGuy()  //  {name: "Joe", talk: ƒ}  lol, still not window object

On a quick note, you shouldn't use arrow functions as methods as they aren't fit for that purpose. This was only for demonstration purposes.


Conclusion

We've covered ample ground on this topic. We started off by getting to know what this was. We looked at functions, the variety of ways they are invoked, and how they affect how this is determined. We also took a good look at arrow functions and their interesting peculiarities.

Hopefully, this article has done enough to demystify and help you understand the this parameter once and for all. Thanks for reading!