`this` is the worst - JavaScript
For a lot of us, one of the toughest parts of JavaScript is this
-- that is, figuring out what this
is. The academic answer is that this
is an "implicit extra parameter" that is automatically passed. You can access it, but you can't mutate it after it's set.
So, at any given point in the code, "What is this
"? The answer is: it depends on a lot of things.
But let's start with the simplest case, code in the global scope.
Example 1: Global scope
console.log(`this in the global scope is ${this}`)
var b = 123;
console.log(`this.b is ${this.b}`);
// console results:
// this in the global scope is [object Window]
// this.b is 123
(Note: console results throughout this post reflect the output seen in Chrome)
When we're not inside a class or an object, this
is what is called the "global object", which in a browser is the Window object.
(Notice that we used var
here instead of let
or const
as provided by ES6. That's because a feature of let
is that it intentionally prevents assigning variables to the global scope. Normally you want to avoid var
and use let
or const
; this example is just to illustrate what this
is in the global scope.)
Next up, let's talk about non-arrow functions, the kind where the function is defined like this:
function foo() {
// function body goes here
}
For functions like these, this
is the thing to which the function belongs, based on how you invoke the function. Let's look at some examples.
Example 2: Within a non-arrow function in the global scope
function foo() {
console.log(`this within foo is ${this}`);
}
foo();
// console results
// this within foo is [object Window]
Since foo
belongs to the global scope (and not to some class or object), this
still refers to the global object, Window
.
Example 3: Within a non-arrow function in an object
let foo = {
x: 10,
whatIsThis: function () {
console.log(`this equals foo? ${this === foo}`);
console.log(`this.x is ${this.x}`);
}
};
foo.whatIsThis();
// console results
// this equals foo? true
// this.x is 10
We've got an object foo
with a property x
that is set to 10, and a property called whatIsThis
that contains a function. The function prints out some information about this
.
If we run foo.whatIsThis()
, we can see that this
is the object to which the function belongs, foo
.
What if we start using that function in other objects, though?
Example 4: Within a non-arrow function defined elsewhere
let foo = {
x: 10,
whatIsThis: function () {
console.log(`this equals foo? ${this === foo}`);
console.log(`this.x is ${this.x}`);
}
};
// the above code is the same as the previous example
let bar = { x: 20 };
bar.whatIsThis = foo.whatIsThis;
bar.whatIsThis();
// console results
// this equals foo? false
// this.x is 20
Something to know about this example is that bar.whatIsThis
is just a reference to foo.whatIsThis
. They're the exact same function in memory, as we can show with:
foo.whatIsThis === bar.whatIsThis // true
Despite this, the values we see printed to the console are different based upon how we call the function. Rather than seeing the same results from Example 3, the printed lines now reflect the fact that we called the function using bar.whatIsThis()
instead of foo.whatIsThis()
.
To summarize, even though we're calling the exact same function, the value of this
depends upon how we call it. It literally depends upon what is on the left side of the .
in the function call.
Okay, now let's get weird. There are 3 situations inside a function where this
just gives up and is set to the global object even though you're inside an object.
These situations are:
When the function is defined as a local variable
When the function is inside another function (a.k.a. "an inner function")
When the function was passed as an argument
Example 5: Within a local variable
let foo = {
bar: function () {
let imALocalVariable = function() {
console.log(this);
}
imALocalVariable();
}
};
foo.bar();
// console results:
// Window
Example 6: Within an inner function
let foo = {
bar: function () {
function imAnInnerFunction() {
console.log(this);
}
imAnInnerFunction();
},
};
foo.bar();
// console results:
// Window
Example 7: When the function is passed as an argument
let foo = {
whatIsThis: function () {
console.log(this);
},
bar: function(ImAParameter) {
ImAParameter();
}
};
foo.whatIsThis();
foo.bar(foo.whatIsThis);
// console results:
// Object { whatIsThis: whatIsThis(), bar: bar() }
// Window
Note that in this example, when we run the function directly with foo.whatIsThis()
, the object that prints to the console is foo
itself. However, when we pass the same function as an argument to another function in foo
that simply calls the passed-in function, this
now refers to Window
.
Next up, let's talk about methods in classes. First: static methods. Static methods are those that are defined on a class, and they're called on the class itself rather than on an instance of the class. They're akin to class methods in Ruby and other languages.
Inside of class methods, this
refers to the class itself.
Example 8: Within a static method
class Foo {
static bar() {
console.log(this);
}
}
Foo.bar();
// console results:
// class Foo {
// static bar() {
// console.log(this);
// }
// }
For instance methods (a.k.a. prototypes method) in a JavaScript class, on the other hand, this
refers to the current instance.
Example 9: Within an instance method
class Car {
constructor(mileage) {
this.mileage = mileage;
}
printMileage() {
console.log(this.mileage);
}
}
const newCar = new Car(5);
const oldCar = new Car(100000);
newCar.printMileage();
oldCar.printMileage();
// console results:
// 5
// 100000
In this example, this
behaves in the way you probably expect within classes.
But, as you may expect, there are exceptions to the behavior of this
within classes.
Example 10: Within event listeners
class Foo {
constructor() {
const button = this.addButton();
button.addEventListener('click', function() {
console.log(this);
});
}
addButton() {
const button = document.createElement('button');
button.innerHTML = 'CLICK ME';
document.body.prepend(button);
return button;
}
}
new Foo();
// console results, after clicking the button:
// <button>CLICK ME</button>
The first exception to the behavior of this
happens in functions used as event listeners. Inside those, by default this
refers to event.currentTarget
. this
still refers to event.currentTarget
if you set the event listener a little differently, like this:
class Foo {
constructor() {
const button = this.addButton();
button.addEventListener('click', this.printThisToConsole);
}
printThisToConsole() {
console.log(this);
}
addButton() {
const button = document.createElement('button');
button.innerHTML = 'CLICK ME';
document.body.prepend(button);
return button;
}
}
new Foo();
// console results, after clicking the button:
// <button>CLICK ME</button>
Example 11: Within an event listener using .bind
class Foo {
constructor() {
const button = this.addButton();
button.addEventListener('click', this.printThisToConsole.bind(this));
}
printThisToConsole() {
console.log(this);
}
addButton() {
const button = document.createElement('button');
button.innerHTML = 'CLICK ME';
document.body.prepend(button);
return button;
}
}
new Foo();
// console results, after clicking the button:
// Foo {}
You may have seen people use .bind
, especially around event listeners like this. A thing you can do with .bind
is to manually set the value of this
. So we use this strategy in the above example to get the this
in the event listener to be the same as the this
in the other methods in that object.
A thing to be careful with about .bind
, though, is that you can never re-bind. The first time you bind a function, it's permanent and un-undoable.
Example 12: Within an arrow-function event listener
class Foo {
constructor() {
const button = this.addButton();
button.addEventListener('click', () => console.log(this));
}
addButton() {
const button = document.createElement('button');
button.innerHTML = 'CLICK ME';
document.body.prepend(button);
return button;
}
}
new Foo();
// console results, after clicking the button:
// Foo {}
Now for another twist. If you use an arrow style function instead of a function() {}
style function for defining an event listener within a class, the value of this
is the same as in other methods: the current instance of the class. Compare the code in Example 12 to that in Example 10 - the only difference is the way the function is defined.
Part of the popularity of arrow functions is due to the fact that it preserves the value of this
; this
will be whatever it was in the context that the function was defined. This fact can make code more consistent and predictable.
Another interesting thing about arrow functions you can't change the value of this
, even with .bind
or other methods like .call
or .apply
.
References
Several of the code examples here were adapted from this excellent chapter by Dmitry Soshnikov. Many thanks to Dmitry for helping me learn all about this
!