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).
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.
There are four ways functions can be invoked in JavaScript, each with its own peculiarity:
averageJoe()
, in which the function is invoked in a straightforward manneraverageJoe.talk()
, which ties the invocation to an object, enabling object-oriented programmingnew AverageJoe()
, in which a new object is brought into beingaverageJoe.call(someObject)
or averageJoe.apply(someObject)
— Secrets of the JavaScript Ninja, Second Edition
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()
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: ƒ}
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()
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"}
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.
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 respectivethis
values, the reasons were different. ForregularFunc
it was because it was invoked in straightforward manner, while forarrowFunc
it was because thewindow
object was the existingthis
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 a 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.
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"
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.
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!
Also Published Here