What is `this`?
Now that I've finished writing about each item on that very long list of array methods, I'm finally getting around to some of the other topics on my Trello board that I've been wanting to put onto this blog. As I've said previously, starting and maintaining this blog is an exercise in breaking down my imposter syndrome when it comes to web development. I love having a written record and time capsule of what I've learned and when I've learned it, and so far writing about a concept, a method, or how I solved some problem has really helped to solidify that knowledge in my own head, and I feel like I've been becoming a better developer in that process.
Today I want to write about one of the more confusing (at least to me) bits of JavaScript: the this
keyword. Over the last three or so months I've been reading Kyle Simpson's series, You Don't Know JS. Stepping back a little bit, I've twice tried reading and coding through Marijn Haverbeke's Eloquent JavaScript. Marijn is clearly a very talented programmer, but for some reason that book just will not click with me. Maybe it's a bit advanced or it just skims over certain topics that need a bit more time and explanation so as to keep them from being speed bumps for beginner developers. The this
keyword is one of those. I got an online referal to try reading the YDKJS books, and while it's still a bit heavy on the theoretical side, I love the deep dive that Kyle takes in talking through weird and misunderstood parts of the language. This blog post is a distillation of some of his fine work in his book, this & Object Prototypes.
As a quick aside, none of this is meant to disparage Eloquent JavaScript or Marijn Haverbeke. It was presented to me as a good book for beginners, but so much of it has just flown completely over my head. I will give it another shot after I've finished YDKJS, so I definitely have not given up on it.
Why learn this
?
So what is this
and why would anyone want to learn about it? I've now written at least my first real world application, and I didn't really use this
, and if I did it was trial and error as to what it was referring to. You see, the this
keyword in Javascript is a reference to some sort of data or functional context. What it refers to depends on where and how a function that uses it has been called, and while it can be difficult to decipher sometimes, proper usage of this
is an elegant way of passing context around your application.
You can get by without it (and I have), but getting by isn't growth, and the second you start looking at code produced by more experienced developers, it can look like pure magic when they're throwing this
around and their code is producing wonderful results. For my part, I felt that limitation when I started looking into the NPM dependencies that my application was using, just to see what was going on under the hood. Trying to recreate and learn from that code's behavior led to me writing console.log(this)
over and over and over and over just to see what in the hell was going on. Clearly, I just didn't understand the convention of what they were writing, and yet ostensibly this is a convention in a programming language that I knew.
The trouble with this
So why is this
so difficult? I think there are two main problems.
- The
this
keyword itself. "This" in the English language is already a vague word, a pronoun that supposes that you know what it refers to, and that's just in a written/spoken language. Now when it appears in a difficult, dynamic programming language, its behavior changes and it can just be straight up quirky and unpredictable at times. Compounding the problem is the the fact thatthis
is just linguistically difficult to write or speak about as a concept. When we start to break the concept down, we often wind up referring to multiplethis
use cases, and start falling into this trap of saying stuff like "thisthis
as opposed to thatthis
." As an English major, it drives me crazy (and I'm sure this blog post will take me forever to copy edit). For what it's worth, when I'm refering to the JavaScript keyword, it will look like so:this
. Otherwise it will be regular font or italicized where appropriate. - Bad teaching about the rules. At least in my coding bootcamp, the instructors did not break down the various rules that dictate what
this
was referring to. Again, allthis
is is a context for a function at the time that function is executing. That context will hold data and other functions. What that context actually is completely depends on how and where that function is called at execution time.
What isn't this
?
There are two common misconceptions that are important to get out of the way first.
this
does not refer to the function itself. Look at the following code:
const names = ["Joey", "Sebastian", "Bad Bart", "Franklin"];
function takeAttendance(name) { console.log(`Hello ${name}!`); this.numberOfAttendees += 1;}
takeAttendance.numberOfAttendees = 0;
names.forEach((name) => { takeAttendance(name);});
console.log(takeAttendance.numberOfAttendees);// result: 0
We're trying to increment the numberOfAttendees
value each time we greet someone in the names
array. But wait a second, we called takeAttendance()
four times, why didn't its count increment? That's because the this
here doesn't point to the function itself. Instead, we need to do some work to actually bind this
to the object we want:
const names = ["Joey", "Sebastian", "Bad Bart", "Franklin"];
function takeAttendance(name) { console.log(`Hello ${name}!`); this.numberOfAttendees += 1;}
takeAttendance.numberOfAttendees = 0;
names.forEach((name) => { takeAttendance.call(takeAttendance, name);});
console.log(takeAttendance.numberOfAttendees);// result: 4
We'll discuss how this works pretty soon.
this
does not refer to the function's lexical scope. Look at the following code:
function randomNumber() { const number = Math.floor(Math.random() * 10); printRandomNumber();}
function printRandomNumber() { console.log(this.number);}
randomNumber();// result: undefined
What's going on here? Well we're trying to pass the number
variable, which exists in the randomNumber()
scope, to a different function, printRandomNumber()
. But even though we call printRandomNumber()
inside of randomNumber()
, printRandomNumber()
doesn't have access to what's in randomNumber()
's scope. No bridge can be made here, and especially not using the this
keyword.
What is this
?
So then what the hell is this
?
this
is a binding made at runtime, and what it refers to is entirely dependent on where, when, and how its function was called to execute.
There are four major binding rules determining what context this
refers to:
- Default Binding
- Implicit Binding
- Explicit or Hard Binding
- new Binding
Default Binding
Default Binding occurs when you're invoking a function at the global scope (often window
in a browser). It can be thought of as the default catch-all rule for this
binding. Look at the following code.
function logColor() { console.log(this.color);}
var color = "Blue";
logColor();// result: "Blue"
The first thing to note is that when you do a plain variable declaration at the global scope, that variable and its value are made into a key-value
pair that exists globally. In a browser this is window
. The following console.log()
s are exactly the same:
var color = "Blue";
console.log(color);console.log(window.color);
Secondly, in the previous example, the logColor()
function resolves to the global (window
) object. If we examine its call site, it's called as a plain function reference, and none of the other rules (that we'll get to shortly) apply.
An important note: if we're working in strict mode
, the global object is not eligible for default binding, so the above code would throw a TypeError
.
function logColor() { "use strict";
console.log(this.color);}
var color = "Blue";
logColor();// result: TypeError: Cannot read property 'color' of undefined
Implicit Binding
The second rule we'll look at is Implicit Binding, which occurs when a context object contains a reference to the function that it's calling. Let's take a look:
function logColor() { console.log(this.color);}
var car = { color: "Blue", logColor: logColor,};
car.logColor();// result: "Blue"
In this code we have declared the logColor()
function, then later added a reference to it as a property on the car
object. This allows the callsite of the function to use the car
context to reference the function, so this.color
and car.color
are the same thing.
This is handy enough, but it has its pitfalls in which this
binding can be lost, or point to something unintended (often it's falling back to Default Binding).
function logColor() { console.log(this.color);}
var car = { color: "Blue", logColor: logColor,};
const logTruckColor = car.logColor;
var color = "Red";
logTruckColor();// result: "Red" -- NOT "Blue"!
So what happened here? For one thing, even though logTruckColor()
references the logColor()
function reference inside of the car
object, it's really just a reference to the original logColor()
function itself. Secondly, what's important when determining this
is the function's call site. In this case, we're calling logTruckColor()
with a plain function call, so Default Binding occurs, and this
points to the global object, on which we have added a color: "Red"
property.
Be especially wary of this behavior when working with callback functions.
Explicit Binding
Where Implicit Binding requires us to mutute the object we're working with to contain a reference to the function we want to use, the third rule, Explicit Binding, allows us to deliberately tell a function what object we want to use. The syntax involves using .call()
or .apply()
methods, which are available on all functions we would declare. The first method each parameter takes is the object you want this
to be a reference to.
function logColor() { console.log(this.color);}
var car1 = { make: "Toyota", model: "Corolla", color: "Blue", year: 2005,};
var car2 = { make: "Toyota", model: "Camry", color: "Red", year: 2001,};
logColor.call(car1);// result: "Blue"
logColor.call(car2);// result: "Red"
Hard Binding
Even using Explicit Binding won't always solve the problem of losing this
binding to global scope (as could occur when we're working in some framework or using some code dependency). But a variation called Hard Binding occurs helps mitigate this issue while allowing you to pass in additional arguments.
function calculateCarTotal(fees) { console.log(`Price: ${this.price}, Fees: ${fees}`); return (this.price += fees);}
var car = { make: "Toyota", model: "Corolla", year: 2005, color: "Blue", price: 1000,};
var bindCarPrice = function () { return calculateCarTotal.apply(car, arguments);};
var corollaPrice = bindCarPrice(200);console.log(corollaPrice);// results:// Price: 1000, Fees: 200// 1200
This pattern is so common that as of ES5 Function.prototype.bind
is a utility built into the language, and it's used like this:
function calculateCarTotal(fees) { console.log(`Price: ${this.price}, Fees: ${fees}`); return (this.price += fees);}
var car = { make: "Toyota", model: "Corolla", year: 2005, color: "Blue", price: 1000,};
var bindCarPrice = calculateCarTotal.bind(car);
var corollaPrice = bindCarPrice(200);console.log(corollaPrice);// results:// Price: 1000, Fees: 200// 1200
Language APIs
A lot of functions and methods built into JavaScript provide an optional parameter that allows this
binding, which almost certainly uses Explicit Binding under the hood. You might remember this syntax from the Array.from()
method:
Array.from(arrayLike[, mapFn[, thisArg]]);
And here was an example of using just the first mapFn
parameter:
const doubleNumbers = Array.from([1, 2, 3, 4], (x) => x * 2);console.log(doubleNumbers);// expected output:// (4) [2, 4, 6, 8]
Now here we can produce the same results using the third thisArg
parameter. When looking at the difference between the following example and the previous one, remember that arrow functions can't use the this
keyword!
const doubleNumbers = Array.from( [1, 2, 3, 4], function (item) { return item * this.multiplier; }, { multiplier: 2 });console.log(doubleNumbers);// will return : [2, 4, 6, 8]
new
Binding
The final rule to observe is new Binding, which occurs when we use the new
keyword to call a constructor.
function Car(make, model, year, color) { this.make = make; this.model = model; this.year = year; this.color = color;}
var corolla = new Car("Toyota", "Corolla", 2005, "Blue");
console.log(corolla.color);// result: "Blue"
Put it all together
So those are the four rules to follow when determining this
, and they're presented here in order from lowest to highest precedence. new Binding will be observed over all other rules, then Explicit Binding or Hard Binding, then Implicit Binding, and if none of those rules apply, it falls back to Default Binding.
Some questions to ask yourself:
(1) Is the function called with new
? If so, observe the new Binding rule. this
refers to the newly constructed object.
function Car(color) { this.color = color;}
var corolla = new Car("Blue");console.log(corolla.color);
(2) Is the function called with .call()
or .apply()
? If so, observe the Explicit Binding rule. this
refers to the object being called or applied to the the function.
function logColor() { console.log(this.color);}
var car = { color: "Blue",};
var corolla = function () { logColor.call(car);};
corolla();
(2.1) Is the function called with .bind()
? Observe the Explicit Binding rule, specifically Hard Binding behavior. this
refers to the object being bound to the function.
function logColor() { console.log(this.color);}
var car = { color: "Blue",};
var corolla = logColor.bind(car);
corolla();
(3) Is the function called with a context object, and does that object have a reference to the function as a property? Observe the Implicit Binding rule.
function logColor() { console.log(this.color);}
var car = { color: "Blue", logColor: logColor,};
car.logColor();
(4) Otherwise fall back on Default Binding, in which (if not in strict
mode) this
will be the Global object.
function logColor() { console.log(this.color);}
var color = "Blue";
logColor();// result: "Blue"
Once again I cannot recommend Kyle Simpson's this & Object Prototypes, and really his entire You Don't Know JS series. It gets into much more nuance than I do here, and explores more use cases, theory, exceptions, and gotchas for the rules surrounding this
.