Alright JavaScript, what the hell. We were cool. Now, I just don't know anymore. I was hanging out with Ruby for the past couple months and I got used to how consistent and sensible self
is. Now I'm back and things are...weird.
What I'm struggling with most is whether or not JavaScript's implementation of this
is a necessary evil with a first-class function language, or just another bad idea that we're stuck with.
I'm going to condense for you some thoughts gleaned from MDN, Kyle Simpson and the ECMA 2020 Spec. (just for fun, search for "this" in the Spec 😂)
For those that aren't already frustrated by this
, let's get you on board the "wtf, JavaScript" train. this
should allow us to write functions with an reference to the owning object without having to hardcode the name of the object.
function Person(name) {
this.name = name;
this.printName = function() { console.log(this.name); };
}
const joe = new Person("Joe");
const sal = new Person("Sal");
joe.printName(); //> "Joe"
sal.printName(); //> "Sal"
So that's pretty damn useful. We've just used this
to attach a property and a method to an object and to return the property using the method.
However, methods in JavaScript are just functions, and functions are values which can be bound to new variables, given as arguments to other functions, and returned from functions. Which means we can do this:
const printName = joe.printName;
printName; //> ƒ () { console.log(this.name); }
printName(); //> (nothing prints)
Now that the function printName
is being called from outside of the object context in which it found the value of this.name
, it has no idea what this.name
should resolve to. But it does actually know what this
refers to:
const name = "Mike"; // declared in Global Scope
printName(); //> "Mike"
It turns out that the global object (as well as modules) has its own this
property. If the function you are executing doesn't have a this
defined by its execution context, it will look default to the global this
.
console.log(this); //> Window { ... }
console.log(window.globalThis); //> Window { ... }
That still isn't that confusing. Let's go deeper.
function anExample() {
console.log(this);
function aNestedExample() {
console.log(this);
}
aNestedExample();
}
anExample();
//> Window { ... }
//> Window { ... }
const anObj = {};
anObj.aMethod = anExample;
anObj.aMethod();
//> { aMethod: f }
//> Window { ... }
And that right there is the heart of the matter. Even though we have attached the anExample
function to an object as a method, the function inside of it is still referring to the global object.
Why?
Turns out there are 2 important factors:
1) Is "use strict"
mode enabled?
2) How is the function invoked?
Is "use strict"
enabled?
There's an additional factor! Let's slightly modify the example above to use "use strict"
"use strict";
function anExample() {
console.log(this);
function aNestedExample() {
console.log(this);
}
aNestedExample();
}
anExample();
//> undefined
//> undefined
const anObj = {};
anObj.aMethod = anExample;
anObj.aMethod();
//> { aMethod: f }
//> undefined
If "use strict"
is enforced, JavaScript will not search outside of the function's execution context for a this
value, so if the function does not define one for itself, this
resolves to undefined
.
How is the function invoked?
If the function is invoked as a function without any object provided as a context for this
to refer to, then this
will refer to the global
object.
function example() {
console.log(this);
}
example(); //> Window { ... }
const anObj = {
property: 5,
method: function() {
console.log(this);
function noThis() {
console.log(this);
}
hasThis();
}
}
anObj.method();
//> { property: 5, method: f }
//> Window { ... }
this
is not lexically scoped. Even if it is contained within a function which has a this
value and is lexically located within an object, it still refers to the global
object.
What actually matters is whether or not the function is invoked as a method:
const anObj = {
property: 5,
method: function() {
console.log(this);
return function noThis() {
console.log(this);
}
}
}
const anotherObj = {};
anObj.method()();
//> { property: 5, method: f }
//> Window { ... }
anotherObj.asAMethod = anObj.method();
//> { property: 5, method: f }
anotherObj.asAMethod();
//> { asAMethod: f }
In this example, even though the function noThis
is lexically within a method on another object which does have a this
when invoked it doesn't have its own this
unless it is invoked as a method on a new object. Otherwise refers to the global
object.
So the actual notation that you use to invoke a method matters.
anObject.method; // Dot Notation: this == true
anObject["method"]; // Computed Access: this == true
asAFunction(); // Function invocation: this == false
The value of this
is generated each time the function is invoked. Any function which isn't invoked as an object method will not generate a this
value.
But there's one final twist. Object generator functions and classes do have a this
value when they are invoked, even though they aren't being called as a method:
class Person {
constructor(name) {
console.log(this);
this.name = name;
}
}
const joe = new Person("Joe");
//> Person {}
function Person(name) {
console.log(this);
this.name = name;
}
const joe = new Person("Joe");
//> Person {}
And that makes sense, they're generating an object and need to be able to reference the object while it is being created in order to add properties and methods to it.
If we define a method on the prototype
, it still refers to the created object correctly, because we're using a method call!
Person.prototype.newMethod = function () {
console.log(this);
}
joe.newMethod(); //> Person { name: "Joe" }
Ok, that's actually kind of sensible, but it sets a trap with nested functions where you aren't calling the nested function as a method.
There's two common solutions to that problem:
1) Save the value of this
as a variable, e.g. that
, _this
, self
or _self
.
const anObj = {
property: 5,
method: function() {
const that = this
function noThis() {
console.log(this);
console.log(that);
}
noThis();
}
}
anObj.method();
//> Window { ... }
//> { property: 5, method: f }
Saving the value of this
allows it to be used with normal lexical scoping rules.
2) Use Arrow Functions
const anObj = {
property: 5,
method: function() {
const that = this
const noThis = () => {
console.log(this);
console.log(that);
}
noThis();
}
}
anObj.method();
//> { property: 5, method: f }
//> { property: 5, method: f }
Arrow functions do not define their own this
value, instead they look outwards up the lexical scope chain for a this
value, acting just like you wish this
would.
However, because they do not define their own this
if you don't nest them inside of function defined with the function
keyword and call that as a method from an object, your arrow function won't know what the value of this
is either:
const anObj = {
method: () => console.log(this)
}
anObj.method(); //> Window { ... }
So if you want to use this
:
1) Use dot .
or bracket [ ]
notation when calling your function.
2) Use function
to define your methods instead of arrow functions.
3) Either bind the value of this
or use arrow functions within your methods for lexical scope.
4) Don't use this
unless you expect to use the function as a method or have prepared for it to refer to the global
object instead.
Ok, but why?
Whether or not JavaScript's solution to this
is a good idea should be judged against the problem that it faces. Unlike languages like Ruby where objects own their methods and those methods are typically defined within or upon the object which self
will refer to, JavaScript doesn't have formal methods.
Methods in JavaScript are actually key:value pairs on an object which stores the reference value to the function object. When calling a method with dot or bracket notation, the expression object.method()
is actually two parts: object.method
and the function invocation. object.method
evaluates to myMethodFunction
which is then is then invoked.
Unlike Ruby, where the object receives a message and determines if it can respond to the message based upon the methods defined upon it, all JavaScript objects know is what keys they have defined and what values correspond to those keys. Since the functions used as methods can be defined anywhere and may be referenced by many different objects, there's no clear owner of any function and JavaScript is forced to determine this
based upon the context by which it was called rather than how it was defined.
So this makes sense. It's complicated, it's tricky, and having this
refer to global
rather than lexical scope is a bad idea, but now that we understand that, we anticipate it and just use that
or =>
instead!
Top comments (4)
Another useful thing to note here..
When trying to obtain 'this' within an arrow function, 'this' could be referred to by the variable name of the object:
With that being said.. would it be better to just forget about the use of this and just use the objects name as a replacement?
disclaimer: Though, I haven't found any note-able pitfalls to this approach yet. I'd encourage you all to do your own research before using this method.
Great post.
For the benefit for visual learners(myself included), the series on egghead is worth the watch.
Wow, those are some excellent videos. Incredibly clear.
I'll have to look into subscribing in order to watch them all, but if the rest of their content is on par it will be worth it.
Thanks!
It's totally worth it. They keep releasing tons of courses over the year. Worth the money.