At some point in your JavaScript journey, you've likely encountered the enigma that is the this
keyword. It's a pain to work with for most developers and is often cited as a source of JavaScript's complexity.
To truly comprehend this
, we must first dispel common misconceptions, a topic I delved into in a previous article. Contrary to popular belief, this
isn't self-referential nor bound by lexical scope. Instead, it serves as a binding that is established when a function is invoked, with its reference determined solely by the function’s call-site. Consequently, this
can adopt various values depending on the context of its invocation.
Understanding JavaScript’s this
requires a willingness to set aside any preconceived notions, particularly those influenced by experiences with this
in other programming languages. With that said, prepare yourself for a paradigm shift as we redefine our understanding of this
in JavaScript.
This article is inspired by Kyle Simpson's book, "this & Object Prototypes," particularly Chapter 2: "this All Makes Sense Now." It aims to simplify the fundamental concept of this
in JavaScript by discussing the four common rules that dictate its behaviour.
this and The Call-Site
Let’s tackle the confusion around this by understanding what it is. 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. It can vary depending on how the function is called or invoked and it references the object that is executing the current function.
Why do we need this
?
Well, simply put, this
allows us to reuse functions in different contexts. Once a function has been defined it can be invoked for different objects using the this
keyword. It lets us decide which objects should be focal when invoking a function or a method.
The Call-Site
A 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.To understand this
binding, we have to understand the call-site: the location in code where a function is called (not where it’s declared). The call-site answers ‘what is this this
a reference to?’.
The call stack helps us locate a function’s call-site. The call stack is a stack that keeps track of which functions are currently running in a program. A function’s call site is in the invocation before the currently executing function. In other words, the-call site for the current function can be found by looking at the function immediately before it in the call stack.
Consider Kyle’s snippet below.
function baz() {
// call-stack is: `baz`
// so, our call-site is in the global scope
console.log( "baz" );
bar(); // <-- call-site for `bar`
}
function bar() {
// call-stack is: `baz` -> `bar`
// so, our call-site is in `baz`
console.log( "bar" );
foo(); // <-- call-site for `foo`
}
function foo() {
// call-stack is: `baz` -> `bar` -> `foo`
// so, our call-site is in `bar`
console.log( "foo" );
}
baz(); // <-- call-site for `baz`
After calling baz()
the call stack would look like baz()
→ bar()
→ foo()
. The call site of foo()
is bar()
since foo()
was called from inside bar()
and the call site for bar()
is baz()
. Understanding this concept helps understand what this
will reference in this
-aware functions—functions that use the this
keyword.
Nothing but Rules
There are four different ways to invoke a function; four rules that determine where this
will point during the execution of a function and each answers ‘what this
is’ differently. Inspecting the call-site determines which of the four rules applies.
Default Binding
In this case, this
typically refers to the global object (window
in browsers, global
in Node.js). However, it's worth noting that in strict mode ('use strict'
), the default binding behaves differently, where this
remains undefined
instead of referring to the global object.
Let’s look at another of Kyle’s examples.
function foo() {
console.log(this.a);
}
var a = 2;
foo(); // 2
Declaring a variable using var
in the global scope, is akin to defining a variable on the global object. When foo()
is called, this.a
resolves to the global variable a
. Here, the default binding for this
applies to the function call, and points this
at the global object.
We can prove this by looking at foo()
’s call-site. foo()
was called without any explicit or implicit binding contexts — rules you’ll learn later on — and so default binding applies here and this
points to the global object.
Strictly this
If strict mode is in effect, this points to undefined
, and not the global object. This prevents unintended manipulations of the global object.
function foo() {
"use strict";
console.log(this.a);
}
var a = 2;
foo(); // TypeError: `this` is `undefined`
We’ve enabled strict mode within the function by adding "use strict";
at the beginning. In strict mode, when a function is called without an explicit binding for this
, this
defaults to undefined
instead of the global object, which happens in non-strict mode.
That being said, if a function declared in non-strict mode is called in strict mode, default binding still applies. Though it’s not recommended to mix the two modes.
function foo() {
console.log(this.a);
}
var a = 2;
(function () {
"use strict";
foo(); // 2
})();
Here, the contents of foo()
are not running in strict mode, therefore default binding applies.
Implicit Binding
Implicit binding is intuitive and widely used in JavaScript. Here, this
behaves similarly to how it does in other programming languages. When a function is accessed as a method of an object, this
refers to the object the function is called on.
function foo() {
console.log( this.a );
}
var obj = {
a: 2,
foo: foo
};
obj.foo(); // 2
The value of this
doesn't refer to the object that directly contains the function but rather to the object used to call the function.
When foo()
is called, it's preceded by an object reference to obj
. According to the implicit binding rule, the object preceding the function call is used as the context for this
. Therefore, this.a
is synonymous with obj.a
.
Object Chaining
Object chaining allows you to access nested properties of objects by chaining multiple property references together. When dealing with object chaining, only the top/last level of an object property reference chain matters to the call-site.
function greet() {
console.log(this.name);
}
var person2 = {
name: "Seven",
greet: greet
};
var person1 = {
name: "Thirteen",
person2: person2
};
person1.person2.greet(); // Seven
Above, person1.person2.greet()
is a chained property reference. Despite person1
containing a reference to person2
, it is person2
that calls the greet
function. Therefore, the this
keyword within greet
refers to person2
, resulting in the log "Seven".
Implicitly Lost
Sometimes, an implicitly bound function loses its binding and falls back to the default binding, which could be either the global object or undefined
, depending on whether strict mode is in effect.
function foo() {
console.log(this.a);
}
var obj = {
a: 2,
foo: foo,
};
var bar = obj.foo; // function reference/alias!
var a = "oops, global"; // `a` also property on global object
bar(); // "oops, global"
In this example, we store a reference to the foo()
function in the variable bar
. It's as if bar
now points to foo
. When we call bar()
, it's called without any binding context, causing this
to resolve to the global object. Consequently, this.a
inside foo()
resolves to the a
property of the global object, resulting in the output "oops, global".
Callbacks
The most common way to lose this bindings is by passing this
-aware functions as callbacks.
function greet() {
console.log(this.name);
}
function sayName(fn) {
// `fn` is just another reference to `greet`
fn(); // <-- call-site!
}
var person = {
name: "Dai Bo",
greet: greet,
};
var name = "Da Chun"; // `name` is also property on global object
sayName(person.greet); // 'Da Chun'
In this example, sayName()
expects a callback function. When person.greet
is passed to sayName()
, note that we are only passing a reference to the greet()
function. Thus, when the callback is invoked within sayName()
, it loses its this
binding and is called as a regular standalone function.
In this scenario, this
will now point to the global object and log “Da Chun” because the var
declaration adds a name
property to the global object.
You’ll see a similar outcome with JavaScript’s built in higher order functions— functions that accept callbacks.
const series = {
name: 'Scissor Seven',
introduce(character) {
return character + ' is from ' + this.name
}
}
const characters = ['Seven', 'Cola', 'Prince of Stan']
const result = characters.map(series.introduce)
console.log(result)
// [ 'Seven is from undefined', 'Cola is from undefined', 'Prince of Stan is from undefined' ]
In this example, series.introduce
is merely a reference to the introduce
method. Despite passing in series.introduce
and clear indications that the introduce
method belongs to the series
object, the call-site for the callback function is within the map()
method.
When you call characters.map(series.introduce)
, you're passing the introduce()
method as a callback to the map()
method, thereby losing the series
context binding. It's crucial to recognize that the map()
method executes the callback function in a different context. When a function is called without a context object, as is the case with the callback function inside map()
, this
defaults to the global object (or undefined
in strict mode).
As a result, this.name
inside the callback function resolves to undefined
, leading to the observed outcome where each character is introduced as being from undefined
. This occurs because this.name
is searching for the name
property on the global object, which doesn't exist, hence resulting in undefined
.
Explicit Binding
While JavaScript's dynamic this
can be advantageous, there are situations where predictability is preferred. Explicit binding methods such as call()
, apply()
, and bind()
allow us to precisely control the this
context of a function call.
These methods operate similarly, but with some nuances:
call()
: Invokes the function with a specifiedthis
context and additional arguments passed individually.apply()
: Invokes the function with a specifiedthis
context and additional arguments passed as an array.bind()
: Returns a new function with thethis
context permanently bound, without immediately invoking it.
Consider this example from Kyle.
function foo() {
console.log(this.a);
}
var obj = { a: 2 };
foo.call(obj); // 2
Here, we use call()
to specify that this
within foo()
should refer to the obj
object.
If you pass a simple primitive value (of
type
string, boolean, or number) as thethis
binding, the primitive value is wrapped in its object- form (new
String(..),new
Boolean(..), ornew
Number(..), respectively). This is often referred to as “boxing.”
Kyle Simpson - this & Object Prototypes
Hard Binding
Explicit binding alone doesn’t offer any solution to implicitly losing this
bindings. Hard binding creates a new function where this
is permanently bound to a specific value. This ensures that the function always executes with the same this
context, regardless of how it's called.
function logWeapon() {
console.log("I use " + this.weapon);
}
const thirteen = {
codeName: "Plum Blossom Thirteen",
age: 17,
rank: 38,
weapon: "Plum Blossom Darts",
};
// Hard binding `this` to the `thirteen` object
var getSkill = function () {
logWeapon.call(thirteen);
};
getSkill(); // 'I use Plum Blossom Darts'
setTimeout(getSkill, 100); // 'I use Plum Blossom Darts'
// Attempting to rebind `this` has no effect
getSkill.call({ weapon: "Swords" }); // 'I use Plum Blossom Darts'
In this example, getSkill()
stores a new function where this
is permanently bound to the thirteen
object. Alternatively, you could achieve the same result using var getSkill = logWeapon.bind(thirteen)
, which also creates a new bound function.
The benefit of hard binding becomes evident when using the function as a callback, such as with setTimeout(getSkill, 100)
. Despite being invoked asynchronously after a delay, the this
context remains consistent due to hard binding. As a result, the function behaves as intended, logging the expected message even in asynchronous scenarios.
It's important to note that hard binding is a one-time operation. Once a function is hard-bound, attempts to rebind its this
context later on won't affect the bound value. This ensures that the function always executes with the same this
context, providing predictability in its behaviour.
Callbacks
In the previous section, we discussed how callbacks often lose their original this
bindings, leading to unexpected behaviour. By default, they're typically called with a this
value of undefined
in strict mode or the global object otherwise.
To address this issue, we can utilize hard binding with the bind()
method.
const series = {
name: "Scissor Seven",
introduce(character) {
return character + " is from " + this.name;
},
};
const characters = ["Seven", "Cola", "Prince of Stan"];
const result = characters.map(series.introduce.bind(series));
console.log(result);
// [ 'Seven is from Scissor Seven', 'Cola is from Scissor Seven', 'Prince of Stan is from Scissor Seven' ]
In this example, series.introduce
loses its this
binding when passed as a callback to the map()
method. However, by using bind()
and explicitly setting the this
context to series
, we create a new function that is permanently bound to the series
object. As a result, the this
context is preserved across all invocations of the callback function.
Partially Applied Functions
We can use bind()
to create functions with preset initial arguments, a technique known as partial application. Partial application involves fixing a certain number of arguments of a function, resulting in a new function with fewer arguments. The partially applied function accepts the remaining arguments and produces the result.
function add (num1, num2){
return this.num + num1 + num2
}
var obj = {
num : 4
}
// both functions bind this to obj
const addTwo = add.bind(obj, 2) // fixing the first argument to 2
const addThree = add.bind(obj, 3) // fixing the first argument 3
addTwo(5) // 11
addThree(5) // 12
In this example, addTwo()
and addThree()
are both bound to the obj
object. The bind()
method allows us to preset arguments for the target function, add()
. For instance, add.bind(obj, 2)
sets the first argument of add
to 2, creating a new function addTwo
with its first argument fixed to 2.
When these partially applied functions are called with additional arguments, such as addTwo(5)
, the bound arguments are combined with the provided arguments to produce the final result. In this case, 5 is passed as the second parameter to add()
, resulting in the expected computation.
New Binding
In JavaScript, the new
keyword serves as a mechanism for creating new object instances based on constructor functions. When a function is invoked with the new
operator, it triggers a specific set of automatic steps to create and initialize a new object.
Unlike in traditional class-oriented languages, where constructors are special methods attached to classes, JavaScript constructors are simply regular functions. However, when invoked with new
, these functions act as constructors, initializing the newly created object with properties and methods defined within them.
Let's explore the behaviour of the new
operator in more detail to understand how it works in JavaScript
Constructors in JavaScript
Constructors are ordinary functions that are invoked with the new
operator, transforming them into constructor calls. They are not attached to classes, nor are they instantiating a class. Unlike in some other programming languages, JavaScript does not have special "constructor functions"; instead, any function can be used as a constructor with the new
operator.
When a function is invoked with new
, also known as a constructor call, JavaScript performs a series of automatic steps behind the scenes to create a new object and initialize its properties using the constructor function.
Let's explore these automatic steps in more detail.
The new Keyword
The purpose of the new
keyword is to invoke a function with a this
keyword pointing at a brand new empty object.
Here are the 4 special steps that JS performs when a function is invoked with new
.
Create a brand new empty object.
Link the
[[Prototype]]
of that new empty object to the function's.prototype
objectInvoke the function with the
this
context set to that new empty object.If the function being invoked with
new
does not explicitly return an object (using areturn
statement), the newly created object from steps 1-3 is implicitly returned.
If the function explicitly returns an object, such as with a return
statement, the newly created object from steps 1-3 is discarded, and the returned object from the function is used instead. That's a tricky gotcha to be aware of, in that it effectively discards that new object before the program has a chance to receive and store a reference to it. Essentially, new
should never be used to invoke a function that has explicit return ..
statement(s) in it.
Consider this code:
function Person(name, age) {
// Step 1: A new empty object is created automatically
// let instance = {}
// Step 2: The newly created object is linked to the prototype of the constructor function
// Object.setPrototypeOf(instance, Person.prototype)
// Step 3: The constructor function is called with the new object as `this`
// let result = Person.call(instance, name, age)
this.name = name;
this.age = age;
// Step 4: The new object is implicitly returned
// result = typeof result !== "object" ? instance : result;
}
const agent = new Person("Zapata", 30);
After calling Person
function with the new
keyword, you can see the steps it take to bind the this
context and return a new object.
Everything in Order
When determining the value of this
in JavaScript functions, if multiple binding rules are applicable at the call-site, precedence is established. The order of precedence for the four types of binding is as follows, from highest to lowest:
new
bindingExplicit binding (including hard binding)
Implicit binding
Default binding
Implicit vs Explicit Binding
Which is more precedent, implicit binding or explicit binding? Let’s check.
function getSeriesName() {
console.log(this.name);
}
var series1 = {
name: 'Castlevania',
getSeriesName: getSeriesName,
};
var series2 = {
name: 'Arcane',
getSeriesName: getSeriesName,
};
series1.getSeriesName(); // Castlevania
series1.getSeriesName.call(series2); // Arcane
series2.getSeriesName.call(series1); // Castlevania
From the snippet, we observe that explicit binding takes precedence over implicit binding.
The crucial point lies in how the function getSeriesName()
is called rather than where it is defined. Despite the fact that getSeriesName()
is originally defined within the series1
object, the call()
method is used in series1.getSeriesName.call(series2)
, explicitly specifying the this
context as the series2
object. This explicit binding overrides the original implicit binding, demonstrating that the manner in which the function is invoked takes precedence over its original definition.
New vs Implicit Binding
Which is more precedent, implicit binding or new binding? Let’s check.
function setEpisodes(episodes) {
this.episodes = episodes;
}
var season1 = {
setEpisodes: setEpisodes,
};
var spinOffSeason = {};
// Setting the number of episodes for the main season
season1.setEpisodes(10);
console.log(season1.episodes); // 10
// Setting the number of episodes for the spin-off season using explicit binding
season1.setEpisodes.call(spinOffSeason, 5);
console.log(spinOffSeason.episodes); // 5
// Creating a new season instance using new binding
var season2 = new season1.setEpisodes(15);
console.log(season1.episodes); // 10
console.log(season2.episodes); // 15
Even though we called new season1.setEpisodes(15)
, this did not change the season1
object because the new keyword preceded the function call. The reason new binding overrides implicit binding is that when a function is invoked with new
, it explicitly creates and returns a new object, ensuring that the function's this
context is bound to that new object, regardless of the surrounding context or how the function is invoked.
Hence, the newly created object is returned and assigned to the season2
variable.
New vs Explicit Binding
We can explicitly bind a this
context using apply()
, bind()
, or call()
. However, when any of these methods are used in conjunction with the new
keyword, the explicitly set context is ignored. However, the provided arguments are still prepended to the constructor call.
function Person(name, age) {
this.name = name;
this.age = age;
}
// creating a new function with a fixed name argument using bind
var obj = { job: "secretary" };
var employee = Person.bind(obj, "Carol Tunt");
// creating a Person instnace with the fixed name "Carol Tunt"
var carol = new employee(32)
console.log(carol.name); // 'Carol Tunt'
console.log(carol.age); // 32
In this example, we have a Person
constructor function that takes two arguments: name
and age
. By using the bind
method, we create a new function called employee()
, which is essentially a partially applied version of the Person
function with the name
argument fixed to "Carol Tunt".
When we use the new
keyword to instantiate a new object (carol
) with the employee()
function, the name
argument is already set to "Carol Tunt", while the age
argument can still be provided dynamically. new
this overrides and ignores the previously set this
context but keeps the previously set arguments. If you call employee()
without new
, the bound this
is suddenly not ignored.
Why is it useful fornew
binding to take precedence over explicit binding?
New binding preserves the fixed arguments while allowing for the flexibility to provide additional arguments during instantiation. Think partial application.
Conclusion
There are more rules as to how this
behaves in other contexts and of course, there are some exceptions to the “rules” but I’ll expand more on those in future articles. For now we’ll focus on the common ones.
We can summarize the rules for determining this
from a function call’s call-site, in their order of precedence. Ask these questions in this order, and stop when the first rule applies.
- Is the function called with
new
? If so,this
is then newly constructed object.
var bar = new foo()
- Is the function is called with
call
,apply
, orbind
? If so,this
is explicitly set to the specified object.
var bar = foo.call(obj2)
- the function is called with a context object
this
refers to that context object.
var bar = obj1.foo()
- Otherwise, its default binding—
undefined
in strict mode, global object otherwise
var bar = foo()
Determining the this
binding for an executing function requires finding the call-site of that function and applying the rules in the **order of precedence above.
Does this
all make sense now?