this & Execution Contexts
This is a place in our discussion that I again ask you to be mindful of any
underlying assumptions or intuitions that you may bring along from your
experience with other programming languages. Because its behavior differs
significantly from that of its counterparts in other languages, JavaScript’s
this
keyword is one of the most widely-misunderstood parts of the language.
Due to its seemingly “mysterious” behavior, many developers approach its use in
a trial-and-error fashion – or, more commonly, find ways to avoid its use
altogether. (It is not difficult to work around, in many cases.)
The next two sections, which cover the this
keyword and prototypes, are
usually the most challenging aspects of the language – especially when we have
preconceived notions to unravel. Our discussion may be fairly dense in spots.
You may wish to skim over these sections to get an overall view before starting
back here again to dive into the details. These mechanisms are powerful tools,
and with our understanding of them we can build useful and efficient design
patterns – so let us not shy away from it!
Let us first point out what this
is not (but is often thought to be):
- The current function object itself
- The current function’s lexical scope
It is true that in JavaScript, functions are objects, so it is not illogical
to initially assume that this
serves as a reference to the currently-scoped
function, or somehow provides a handle to the current lexical scope; however,
this is not the case.
The value of this
is dependent upon how the function was invoked, not
where the function appears in code. Thus, this
– on the exact same line of
code in the exact same function – can take on different bindings at different
points in your program’s execution, depending on how said function is invoked.
The binding of this
is entirely determined at run-time based on the current
execution context. It is unrelated to the concept of lexical scope.
For example, consider the following code wherein we try to track how many times
the function foo
is called:1
function foo(num) {
console.log(num); // Keep track of how many times foo is called
this.count++;
}
foo.count = 0;
for (let i = 0; i < 5; i++) {
foo(i); // 0 // 1 // 2 // 3 // 4
}
console.log(`foo was called ${foo.count} times.`); // "foo was called 0 times" <-- !??
Apparently, this.count
does not reference the same thing as foo.count
, as
one might have assumed. So, then… what is this
?
Execution Contexts
In order to understand how this
is bound, we must first understand the concept
of execution contexts. When your program begins to run, the JavaScript engine
creates and enters the global (or “base”) execution context. Whenever a function
is invoked at run-time, a new execution context is created, pushed onto the top
of the execution stack, and immediately entered. The engine then runs through
this execution context (possibly entering more new subsequent contexts) until it
reaches the end, at which point the context is popped off of the stack and
execution of the previously underlying context (which is now once again the top
of the stack) continues.
We must look at a function’s call-site in order to determine what this
will
be bound to at run-time. That is, the binding of this
inside of a given
function, during a given execution context, is determined by how the current
context was created by the context immediately below it on the execution stack.
Default Binding
By default, a function invoked by a standalone function invocation binds
this
to the global object.
We can imagine how a misinformed use of this
may accidentally end up altering
global state – not good. In strict
mode, this
instead defaults to
undefined
when outside of the global context.
'use strict';
function foo() {
console.log(this.a);
}
let a = 42;
foo(); // throws TypeError: Cannot read property 'a' of undefined
Implicit Binding
An implicit binding is given when a function is invoked through a context
object; that is, the function is invoked through a reference on a containing
object using a property accessor with dot or array notation. If a function is
invoked as such, this
is implicitly bound to the context object itself.
function foo() {
console.log(`Hi, I'm ${this.name}.`);
}
foo(); // "Hi, I'm undefined."
let alice = {
name: 'Alice',
greet: foo,
};
alice.greet(); // "Hi, I'm Alice."
let bar = alice.greet;
bar(); // "Hi, I'm undefined."
As we can see, when the function foo
is accessed and invoked through the
containing object alice
, the object alice
is itself bound to this
– but
only during that specific execution context of the function. Notice that this
implicit binding appears to be “lost” (rather, it is never actually created) if
we then later call the same function using a different reference, such as on
line 16 above. Because of how we initialized bar
on line 14, it might appear
to somehow reference the function foo
“through” the property alice.greet
;
however, it is actually just another direct reference to foo
in memory, and
the standalone invocation on line 16 will fall back to the default binding rule
for this
.
Another common way to accidentally “lose” an implicitly-bound this
is through
subsequent function calls, which create new execution contexts on top of the
stack.
function foo() {
// --> new execution context for foo
bar();
function bar() {
// --> new execution context for bar
console.log(`Hi, I'm ${this.name}.`);
}
}
let alice = { name: 'Alice', greet: foo };
alice.greet(); // "Hi, I'm undefined."
Remember: the binding of this
depends only on the immediately preceding
execution context. Always look at the function’s call-site.
There is, however, a way to make this code work even with nested function
executions. At the very beginning of the outer function foo
, we can lexically
“capture” the contextual value of this
by assigning it to a new variable.
Sometimes, programmers will call the variable self
or that
.
function foo() {
// --> new execution context for foo
let that = this;
bar();
function bar() {
// --> new execution context for bar
console.log(`Hi, I'm ${that.name}.`);
}
}
let alice = {
name: 'Alice',
greet: foo,
};
alice.greet(); // "Hi, I'm Alice."
Now, all of the scope contained within foo
has access to the variable that
according to the normal rules of lexical scope.
Explicit Binding
In JavaScript, functions have a set of utility methods which we can use to
explicitly (and arbitrarily) set the binding of this
inside of a function for
a given execution: call()
and apply()
. Both of these methods work in a
similar fashion.
Let’s take a look at call
’s method signature:
func.call(thisArg[, arg1[, arg2[, ...]]])
The first argument, thisArg
, is the value of this
provided for the
invocation of function func
. After thisArg
, call()
optionally takes any
number of parameters to pass to func
as normal. If func
returns a value,
that value is in turn returned by func.call()
.
function foo(salutation) {
return `${salutation}, I'm ${this.name}.`;
}
foo(); // "undefined, I'm undefined."
let alice = {
name: 'Alice',
};
foo.call(alice, 'Hi'); // "Hi, I'm Alice."
foo.call(alice, 'Hello'); // "Hello, I'm Alice."
For a function operating in non-strict
mode, if thisArg
is null
or
undefined
, this
will be bound to the global object; if thisArg
is another
primitive (string
, number
, or boolean
), it will be “boxed” in a native
object wrapper of its respective type. In strict
mode, the value for thisArg
is simply passed to the function as-is.
Here is the method signature for apply()
:
func.apply(thisArg, [argsArray])
apply()
likewise takes thisArg
as the first argument. The optional second
argument accepts a single array, the elements of which will be passed
(“applied”) to func
as normal function parameters.
function foo() {
console.log(`${this.name}'s favorite foods are:`);
for (let arg of arguments) {
console.log(arg);
}
}
let alice = {
name: 'Alice',
foods: ['apple', 'banana', 'carrot'],
};
foo.apply(alice, alice.foods);
// "Alice's favorite foods are:"
// "apple"
// "banana"
// "carrot"
alice.speak = foo;
alice.speak(...alice.foods);
// "Alice's favorite foods are:"
// "apple"
// "banana"
// "carrot"
let bob = {
name: 'Bob',
foods: ['pizza'],
};
alice.speak.apply(bob, bob.foods);
// "Bob's favorite foods are:"
// "pizza"
As you can see, explicit bindings – using call()
or apply()
– take
precedence over implicit bindings to contextual objects.
We can also create a hard binding using a function’s bind()
method. Using
bind()
does not invoke the function; rather, it creates and returns a new
bound function that essentially “wraps” the original function. Here is its
method signature:
func.bind(thisArg[, arg1[, arg2[, ...]]])
The returned bound function can then later be invoked in any context but still
have its inner this
set to the value of thisArg
. If any additional arguments
are given to bind()
, they will be prepended, in sequence, as the first
parameters on the bound function whenever it is invoked.
function foo() {
console.log(`${this.name}'s favorite foods are:`);
for (let arg of arguments) {
console.log(arg);
}
}
let alice = {
name: 'Alice',
};
let bar = foo.bind(alice, 'banana', 'carrot');
bar('apple');
// "Alice's favorite foods are:"
// "banana"
// "carrot"
// "apple"
let bob = {
name: 'Bob',
foods: ['pizza'],
speak: bar,
};
bob.speak();
// "Alice's favorite foods are:"
// "banana"
// "carrot"
bob.speak.apply(bob, bob.foods);
// "Alice's favorite foods are:"
// "banana"
// "carrot"
// "pizza"
So, altogether, hard-bound bindings take precedence over explicit bindings, which take precedent over implicit contextual bindings, which take precedent over the default binding.
Arrow Functions
Also important to note: arrow functions do not bind their own value for the
this
keyword. Effectively, the value of this
inside of an arrow function
takes on the same binding as it would in its lexically-closest-contained,
non-arrow-functional scope.
function sayName() {
console.log(this); // IIFE
(() => {
console.log(this.name);
})();
}
let alice = {
name: 'Alice',
talk: sayName,
};
alice.sayName();
// "{ name: 'Alice', talk: [Function: sayName] }"
// "Alice"
That’s a lot of rules to in mind! In practice, though, you will only need to use
the this
keyword in special circumstances – namely, when creating functions
that can be delegated to by multiple objects and which need access to each
individual object’s state. (We will discuss delegation in the next chapter.) For
a large amount of the code you write, you will probably not have to think about
this
.
-
Adapted from an example in Simpson, this &object prototypes, Ch. 1
↩