this or That? What This Isn't.
#this, #javascript, #objects, #functions, #scope
Mar 13, 20248 min read
The JavaScript this
keyword often finds itself at the center of debates among developers, labeled as a source of confusion that contributes to JavaScript's perceived complexity. However, delving into its intricacies reveals that this
isn't as complex as it's made out to be, provided one grasps its nuances.
Though, to truly comprehend what this
is, we must first understand what it isn’t.
This article draws inspiration from Kyle Simpson's book, this & Object Prototypes, specifically reflecting on insights gained from Chapter 1: "this or That?". Here, I aim to distill my understanding of this fundamental JavaScript concept, shedding light on its true nature amidst common misconceptions.
Why this?
Why do we need this?
this
lets us reuse functions against multiple context objects. This means we can use the same function on different objects without the need to create separate functions for each object.
I’ll use the example Kyle provided.
var me = {
name: 'Kyle',
};
var you = {
name: 'Reader',
};
function identify(context) {
return context.name.toUpperCase();
}
function speak(context) {
var greeting = "Hello, I'm " + identify(context);
console.log(greeting);
}
identify(you); // READER
speak(me); // Hello, I'm KYLE
Here we’re simply passing the objects, me
and you
into the speak
and identify
functions. This implementation works fine, for now. However, as your codebase scales, you might encounter challenges. For instance, if you have numerous functions that need access to the object's properties, you'll end up duplicating the passing of the object into each function. This not only leads to code duplication but also makes maintenance cumbersome.
The alternative, using this
, is a cleaner, smoother way to achieve the same result.
var me = {
name: 'Kyle',
};
var you = {
name: 'Reader',
};
function identify() {
return this.name.toUpperCase();
}
function speak() {
var greeting = "Hello, I'm " + identify.call(this);
console.log(greeting);
}
identify.call(me); // KYLE
identify.call(you); // READER
speak.call(me); // Hello, I'm KYLE
speak.call(you); // Hello, I'm READER
By using this
, we can pass along the object reference without passing the object itself, thereby avoiding code duplication and simplifying maintenance as the codebase grows.
What this isn’t
We haven’t completely tackled what this
is, but let’s talk about what it isn’t.
this
is not Itself
this
does not reference itself. It is a common misconception that this
refers to the function itself. Although we may want to reference a function from within itself, through recursion or having an event handler that can unbind itself when it’s first called, this
isn’t the way to achieve that.
Trying to add self-referencing properties inside the function fails because of this misconception.
Let’s look at another one of Simpson’s snippet.
function foo(num) {
console.log('foo: ' + num);
// keep track of how many times `foo` is called
this.count++;
}
foo.count = 0;
var i;
for (i = 0; i < 10; i++) {
if (i > 5) {
foo(i);
}
}
// foo: 6
// foo: 7
// foo: 8
// foo: 9
// how many times was `foo` called?
console.log(foo.count); // 0 -- WTF?
This function is a poor attempt at trying to record how many times the function was called. We add a count
property to foo()
— we can do that because all functions are objects, just callable objects — to keep track of how many times foo()
was called, but count
remains 0
even though it’s clear it was called four times.
Why didn’t this work?
this
isn’t pointing to foo()
. Like Kyle said ‘The frustration stems from a too literal interpretation of what this (in this.count++
) means.’
For the
this.count
reference inside of the function,this
is not in fact pointing at all to that function object, and so even though the property names are the same, the root objects are different, and confusion ensues.Kyle Simpson - this & Object Prototypes
So if we aren’t incrementing the correct count
property, which count
did we increment?
After the function is called, this
inside foo()
points to the global window object, and this.count
is adding a count
property to said object with a value of undefined
initially.
So even though we added foo.count
, nothing acts on it. Though if you look at the window object you’ll see it has a value of NaN
because subsequent calls to foo()
tries to increment undefined
.
To avoid this issue, you might be tempted to store the variable in a top-level variable.
function foo(num) {
console.log('foo: ' + num);
// keep track of how many times `foo` is called
data.count++;
}
var data = { count: 0 };
var i;
for (i = 0; i < 10; i++) {
if (i > 5) {
foo(i);
}
}
// foo: 6
// foo: 7
// foo: 8
// foo: 9
// how many times was `foo` called?
console.log(data.count); // 4
We’ve solved the problem thanks to lexical scope, but did we really? We’ve ignored the real problem which is that we don’t know how this
behaves.
To solve this, we should consider forcing this
to point to the actual foo()
function object.
function foo(num) {
console.log('foo: ' + num);
// keep track of how many times `foo` is called
// Note: `this` IS actually `foo` now, based on
// how `foo` is called (see below)
this.count++;
}
foo.count = 0;
var i;
for (i = 0; i < 10; i++) {
if (i > 5) {
// using `call(..)`, we ensure the `this`
// points at the function object (`foo`) itself
foo.call(foo, i);
}
}
// foo: 6
// foo: 7
// foo: 8
// foo: 9
// how many times was `foo` called?
console.log(foo.count); // 4
By using call
we are able to specify the value to use as this
when calling foo()
. Here we are setting this
to point to foo()
.
this
is not its Scope
this
does not refer to a function’s lexical scope.
function foo() {
var a = 2;
this.bar();
}
function bar() {
console.log(this.a); //undefined
}
foo();
There are a couple mistakes in this snippet.
- We are trying to reference
bar()
, by callingthis.bar()
. This only works because function declarations in the global scope, are added to the global window object my default. This would’ve failed hadbar()
been a function expression. bar()
is attempting to gain access tofoo()
’s lexical scope by usingthis
. In other words, its trying to gain access toa
by usingthis
. What happens instead, is that insidebar()
,this
points to the window object where thea
property does not exist. Accessing a non-existent property on an object producesundefined
.
The developer who writes such code is attempting to use this to create a bridge between the lexical scopes of
foo()
andbar()
, so thatbar()
has access to the variable a in the inner scope offoo()
. No such bridge is possible. You cannot use athis
reference to look some‐thing up in a lexical scope. It is not possible.Every time you feel yourself trying to mix lexical scope look-ups with
this
, remind yourself: there is no bridge.Kyle Simpson - this & Object Prototypes
What’s this?
We know what this
isn’t — itself nor it’s scope— so what is this
?
this
is a special identifier keyword that’s automatically defined in the scope of every function.
this
is not an author-time binding but a runtime binding. It is contextual based on the conditions of the function’s invocation.this
binding has nothing to do with where a function is declared, but has instead everything to do with the manner in which the function is called.Kyle Simpson - this & Object Prototypes
To put it simply, we won’t know the value of this until the function is called. this
is not something we can tell the value of by just looking at the code. The value of this
is not determined during the function's definition but rather during its execution.
When a function is called, its execution context is created. The execution context has information about where the function was called from (the call-stack), how the function was invoked, what parameters were passed, etc. One of the properties of this record is the this
reference, which will be used for the duration of that function’s execution.
Conclusion
To understand what this
is, we must first understand what it isn’t. this
is neither a reference to the function itself, nor is it a reference to the function’s lexical scope.
this
is actually a binding that is made when a function is invoked, and what it references is determined entirely by the call-site where the function is called.
Back to Blogs