this All Makes Sense Now

this All Makes Sense Now

How the this keyword works in JavaScript

·

17 min read

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 specified this context and additional arguments passed individually.

  • apply(): Invokes the function with a specified this context and additional arguments passed as an array.

  • bind(): Returns a new function with the this 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 the this binding, the primitive value is wrapped in its object- form (new String(..), new Boolean(..), or new 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.

  1. Create a brand new empty object.

  2. Link the [[Prototype]] of that new empty object to the function's .prototype object

  3. Invoke the function with the this context set to that new empty object.

  4. If the function being invoked with new does not explicitly return an object (using a return 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:

  1. new binding

  2. Explicit binding (including hard binding)

  3. Implicit binding

  4. 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.

  1. Is the function called with new ? If so, this is then newly constructed object.
var bar = new foo()
  1. Is the function is called with call, apply, or bind? If so, this is explicitly set to the specified object.
 var bar = foo.call(obj2)
  1. the function is called with a context object this refers to that context object.
var bar = obj1.foo()
  1. 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?