There are three things you can watch forever: fire burning, water flowing, and frontend developers writing yet another article about this.
Still, that pile of articles exists for a reason. The topic really is confusing for a lot of people. There are plenty of good articles that get it right, but mostly from a purely practical angle, without digging into how it actually works underneath. Articles that go deep on the theory are literally a handful.
In this article I'll try to open the topic up from the theoretical side, starting from the ECMAScript specification, and then derive the practical rules for determining this from that theory.
The Reference Record type
One of the foundational concepts for determining the value of this is the Reference Record.
A Reference Record is one of the ECMAScript specification types (not to be confused with JavaScript's own types, or the spec pedants will come for you) used internally by the language. It has the following fields: Base, ReferencedName, Strict, and ThisValue. Let's go through them in order.
Base — a value equal to one of the JavaScript data types, or an Environment Record (another specification type).
ReferencedName — a String, Symbol, or Private Name.
Strict — a Boolean.
ThisValue — one of the JavaScript data types.
In plain terms: Base can be, for example, an object or an Environment Record (essentially a special object that holds all the local variables); ReferencedName is the name of an object property or an identifier; Strict indicates strict mode; and ThisValue is related to the super keyword.
From here on I'll leave out the Strict and ThisValue fields unless we need them for something specific.
So how do we tell what counts as this type? It's fairly simple. The interpreter treats something as a Reference in two cases:
- When we use a property accessor, meaning dot or bracket notation:
obj.name,obj['name'], orobj.method(). Incidentally, the usual phrasing aboutthisis that it equals whatever sits to the left of the dot, which conveniently forgets the bracket variant.
- And when we're dealing with an identifier — variables, properties, or functions.
In every other case — with operators (x + y, x ? y : z, !x) and literals ("abc", [1, 2, 3], and so on) — we get other types.
For example, say we have a person object:
const person = {
name: 'Dasha',
catName: 'Thomas',
}
person.catName
Walking through this code, the interpreter sees that a property accessor is being used to reach the catName property, so what we have is a Reference Record expression that looks like this:
{
Base: person,
ReferencedName: catName,
}
Or take a variable:
const cat = 'Thomas'
That's an identifier, so it's a Reference too, and it looks like this:
{
Base: global object,
ReferencedName: cat,
}
A note on the global object: it's the object that, depending on the environment and on strict mode, can have different values: in a browser it's window, or undefined in strict mode. Rather than list every possible value each time, I'll just use the word "global" from here on.
So we can say that the value of Base is the context that ReferencedName lives in.
this and the Reference type
Now that we've worked out what a Reference Record is, we can move on to how it helps us determine the value of the this keyword.
The point is that the main case where the value of this depends on context is when we're dealing with a Reference, and the value we use is the Base field.
To see how that works in practice, let's look at a few examples.
const person = {
name: 'Dasha',
catName: 'Thomas',
getCatName() {
return this.catName;
}
}
person.getCatName()
The object's method is called through a dot, which means we have a Reference where Base equals person and therefore this equals person.
const getCatName = person.getCatName
getCatName()
And what happens here? Calling the identifier getCatName is also a Reference, but Base will be global, and therefore this equals global.
All of this follows from the spec's algorithm for calling a function. In the first example we passed the check at the first step, whose first sub-step (IsPropertyReference means we're calling a property of an object) leads to taking this from Base.
In the second example we land in the second sub-step, since the value isn't a property reference, and Base is set to the Environment Record, which in this case is the global object.
Now let's look at a more complex, classic example:
;(person.getCatName)();
(false || person.getCatName)();
false || person.getCatName();
In the first case the expression uses the grouping operator (), which on its own doesn't affect the returned value, so the Reference type is preserved and the function returns 'Thomas'.
In the second case grouping is used as well, but with the || operator added, which forces the expression to be evaluated and the Reference type to be lost, so this becomes global. On top of that, in strict mode, trying to execute the line this.catName gives an error, because this will be undefined and we're effectively reaching into undefined.catName.
In the third case an operator is used, but without the parentheses, so the result is a call to person.getCatName() in dot notation with the Reference type intact, and the function returns 'Thomas'.
This tells us that calling the same function in different ways can produce different values of this.
Callbacks deserve a separate mention: there, the value of this depends on the specific function the callback is passed to. That's especially true for external APIs like addEventListener() or setTimeout(), which are part of the HTML specification, where the value of this is something you simply have to memorize rather than derive from the rules.
For example, this inside a click handler callback on document returns document itself — the element the listener is attached to:
document.addEventListener('click', function() {
console.log(this)
})
Whereas inside forEach, this is already global:
[1, 2, 3].forEach(function() {
console.log(this)
})
Arrow functions
Arrow functions are a case of their own when determining this, because according to the specification they simply don't have one, they take it from the parent lexical environment:
It follows that for arrow functions, the value of this is fixed not when the function is called but when it's created. And that can lead to some unexpected results.
Let's go through a few examples.
const person = {
catName: 'Thomas',
getCatName: () => {
return this.catName
}
}
person.getCatName()
The parent lexical environment for the arrow function getCatName here is the outer global Environment Record. Since an object literal (creating an object directly with {}) doesn't create a lexical environment of its own, the arrow function takes this from wherever the person object was created. So calling person.getCatName() returns undefined, or throws in strict mode.
What if we wrap the arrow function call in a regular function?
const person = {
catName: 'Thomas',
getCatName: () => {
return this.catName
},
wrapperFoo() {
return this.getCatName()
}
}
person.wrapperFoo()
Nothing changes. this for getCatName() has already been set statically, and it doesn't matter how or where we call it.
But if the arrow function is defined inside a regular function to begin with, then this comes from that regular function, and calling person.wrapperFoo2() returns 'Thomas':
const person = {
catName: 'Thomas',
getCatName: () => {
return this.catName
},
wrapperFoo() {
return this.getCatName();
},
wrapperFoo2() {
return () => {
return this.catName;
}
}
}
person.wrapperFoo2()();
And here's an example that highlights the fact that this is fixed when the function is created rather than when it's called:
const person = {
catName: 'Thomas',
wrapperFoo() {
return () => {
return this.catName;
}
}
}
const foo = person.wrapperFoo()
foo(); // ‘Thomas’
Remember how, when we were talking about the Reference type, we said this kind of assignment and calling a function without dot notation loses this? For an arrow function that's not a problem, because the value of this was already fixed at creation time and doesn't depend on how the function is called.
That's exactly what makes arrow functions useful as callbacks, which, as mentioned above, can end up with all sorts of values for this.
It also affects bind(), call(), and apply(), which don't override this for arrow functions. That's spelled out in the spec for these methods too:
Methods bind, call, apply
These methods let you set this explicitly when calling a function, passing it as the first argument. But bind() occupies a special place among them, because it produces a bound function. Its defining feature is a fixed this value held in the BoundThis field.
Let's look at an example:
const cat = {
name: 'Thomas',
getOwnerName() {
return this.ownerName
}
}
const owner = {
ownerName: ‘Dasha’,
}
const boundGetOwnerName = cat.getOwnerName.bind(owner)
boundGetOwnerName()
By binding owner as the first argument to bind(), we set it as the value of this, and it stays tied to that function regardless of how the function is called. Even if it's later called with call() or apply(), they can no longer override the this that bind() locked in:
const anotherOwner = {
ownerName: 'Ilia',
}
boundGetOwnerName.apply(anotherOwner)
boundGetOwnerName.call(anotherOwner)
This follows from the spec too, which says that when these methods are applied to a bound function, the this argument is ignored:
call() and apply()
So in our example the cat always stays attached to the original owner we tied it to. And even if we try to override this with bind() a second time, we won't manage it.
There is one exception to this rule, though: the this value passed to bind() is ignored if the function is invoked with the new operator. Per the spec, that's because the constructor path, [[Construct]], simply has no step where BoundThis is applied, unlike the ordinary [[Call]] path:
The new operator
With new, everything looks straightforward at first glance: this takes the value of the newly created object that the constructor returns:
function Cat() {
this.name = 'Thomas'
}
const newCat = new Cat()
newCat.name // ‘Thomas’
Or an empty object, if the function is just called with new:
function Cat() {
console.log(this)
}
new Cat() // {}
Another example, with a class:
class Cat {
constructor(name) {
this.name = name
}
getCatName() {
return this.name
}
}
const cat = new Cat(‘Thomas’)
cat.getCatName(); // Thomas
const cat2 = new Cat(‘Lesya’)
cat2.getCatName(); // Lesya
But there's a subtlety hidden in the phrase "the object that is returned". If the function being called explicitly returns an object, that object becomes the result of new, and the object created by the constructor is lost:
function Cat() {
this.name = 'Thomas'
return {
name: 'Lesya',
getName() {
return this.name
}
}
}
const newCat = new Cat()
newCat.getName() // ‘Lesya’, not ‘Thomas’
Here's why. When new Cat() is called, the engine runs the internal [[Construct]] method:
ConstructorKind is checked, and if this is a base constructor (that is, no
extends), OrdinaryCreateFromConstructor is called. A new empty object is created and givenCat.prototypeas its prototype. That object is stored inthisArgument.OrdinaryCallBindThis takes the created object and binds it as
thisinside the function body. From that point on, every reference tothisin the constructor body points at this new object.OrdinaryCallEvaluateBody executes the function's code. This is where
this.name = 'Thomas'fires, the property is written to the object from step 1.If the function explicitly returned an object (
result.[[Value]]is an Object), that object becomes the result ofnew. The object from step 1 is lost.If the function returned a primitive, or returned nothing,
thisArgumentis returned, the object from step 1.
It's step 4 that explains the last example. An object is returned there, so step 4 fires and the this.name = 'Thomas' from the constructor body is lost.
Finally, the super keyword. Two things are worth pointing out here.
First: in a derived constructor you can't touch this before super() has been called, doing so throws an error.
Second: even when we call a method through super, say super.getName(), this inside it won't refer to the object super points at:
class Parent {
constructor() {
this.name = 'Parent'
}
getName() {
console.log('Name:', this.name)
}
}
class Child extends Parent {
constructor() {
super();
this.name = 'Child'
}
testSuperMethod() {
super.getName()
}
}
const child = new Child()
child.testSuperMethod() // "Name: Child", not “Name: Parent”
So even when methods are called through super, the this context stays bound to the current object.
And this brings us back to the Reference type, incidentally, because evaluating super.getName() ultimately gives us a Reference with the ThisValue field set and Base holding the current object where super was called:
There's one more case worth covering separately. We've already seen that an arrow function in an object literal loses this:
const cat = {
name: 'Thomas',
getName: () => this.name
}
cat.getName() // undefined
You might expect classes to behave the same way. But they don't:
class Cat {
name = 'Thomas'
getName = () => this.name
}
const cat = new Cat()
const fn = cat.getName
fn() // 'Thomas' — this is not lost
Why? As we know, an object literal {} doesn't create a lexical environment of its own, so the arrow function takes this from outside. But properties declared directly in the class body — class fields — are initialized inside the constructor, where this is already bound to the new instance. It's effectively equivalent to this:
class Cat {
constructor() {
this.name = 'Thomas'
this.getName = () => this.name
}
}
The arrow function captures this from the constructor and keeps it forever, like any other arrow function.
Conclusion
Let's boil the whole article down to a short algorithm:
Arrow function? →
thisis taken from the outer lexical environment at creation time. Ignores bind/call/apply, and can't be called withnew.Called with
new? →thisis the new object (unless the function explicitly returns a different object). Overrides bind.Called through bind? →
thisis BoundThis. Not overridable via call/apply.Called through call/apply? →
thisis the first argument.Called through a dot or brackets? →
thisis the object to the left of the dot. Exception:super.method(), wherethisstays the current object rather than the prototype.Everything else →
thisis global (orundefinedin strict mode).
That's it. We've covered all the main cases and some of the edge cases for determining this, working from the operations described in the ECMAScript specification, which should help build a more fundamental understanding of the topic, and with it, help solve concrete problems in practice.











Top comments (0)