Understanding the Invocation Context in JavaScript

Image for post
Image for post

In JavaScript, functions are objects which provide a modular way to define a set of procedures or operations that can be used many times within a program. By using functions, code is abstracted away reducing potential repetitive logic and helping to maintain legibility. But when working with functions, it is important to know a few things about them, like the fact that they have access to many values that affect how the function can be used. For example, a common value that functions have access to are parameters, which are used as local variables within the code block of the function. Parameters are used to store input data which are then used within the function’s code block. Another not so common value that functions have access to are same name variables used to store the function object itself. This value is useful in recursion, but simply put, if a function has a name, then the function’s code block will implicitly contain a variable of the same name storing the function object which can be used to reference itself.

// function is named "factorial"
function factorial(x) {
if (x <= 1) return 1;
// "factorial" variable contain the function object
else return x * factorial(x-1);
}

The example above demonstrates how the variable with the same name as the function is used within the function’s body to recurse or call the function object.

Another important value that functions have access to is what is known as the function’s invocation context, or the “this” keyword of the function object. Depending on where the function is defined, the “this” keyword for the function will have different values. When functions are defined within an object, as properties, they are known as methods. Because a method is defined within the context of a defined object, the invocation context for the method refers to the defined object.

const wolf = {
name: "Balto",
howl() { console.log(this.name + " howls!"); }
};

In this example, the howl() method, or function defined as a property within the “wolf” object accesses its invocation context, “this” keyword, which refers to the object it is defined within. The howl() method then logs to the console the name of the object “this.name” or “Balto” and concatenates the string “ howls!”. However, issues do occur when a nested function is thought to share the same “this” as the outer level method. For example:

const wolf = {
name: "Balto",
howl() {
function gaze() {
console.log(this.name + " gazes at the moon");
}
console.log(this.name + " howls!");
gaze();
}
};

In this example, the nested gaze() function within the howl() method is trying to utilize its invocation context as if it were the same as the howl() method invocation context. However, because the gaze() function is defined in the context of another function, the howl() method’s “this”, depending on whether the code is executing in strict mode or non-strict mode, will be either undefined or the global object.

Prior to arrow functions, many workarounds were used for this issue which consisted of storing the outer level function or method’s “this” in a variable to be used within any nested functions. Another workaround included expressing the nested function as an IIFE or immediately invoked function expression and binding the “this” of the outer level method. For example:

// using bind() method with IIFE
const wolf = {
name: "Balto",
howl() {
const gaze = (function() {
console.log(this.name + " gazes at the moon");
}).bind(this);

console.log(this.name + " howls!");
gaze();
}
};

In these examples, you can see mainly how nested functions were able to maintain the invocation context of the outer method in which they were defined. However, arrow functions simplified this problem due to the value of their “this”. Unlike functions defined with the “function” keyword, arrow functions do not define their own “this” and instead inherit the invocation context of the environment in which they are defined, like an object’s method. So using the example above:

const wolf = {
name: "Balto",
howl() {
const gaze = () => (
console.log(this.name + " gazes at the moon")
);

Because the arrow function expression gaze() does not define its own “this” and instead inherits the outer method howl() invocation context, it can refer to the wolf object and access its name property. This makes arrow functions great for use as callbacks in order to maintain the desired “this” but it also makes them the wrong choice for defining methods. For example:

const wolf = {
...
bark: () => { console.log(this.name + " refuses to bark"); }
};

Because the arrow function is defined within the context of the wolf object and not within a method with a defined “this” that points to the wolf object, it inherits an invocation context that points to the global object in non-strict mode, or undefined in strict mode.

In general, arrow functions are the modern approach if you need to include some nested functions, perhaps helper functions that require access to the invocation context of the outer method. It is also useful to understand how the “this” differs depending on the context of the function’s definition, and the various approaches that aid in guarding against referring to the unintended “this”. For example, if working in React, arrow functions are highly useful when passed to event listeners, especially if they need to access the instance’ state. So with that, next time you’re working with the “this” keyword of a function, remember the context of the function’s definition and avoid any frustrations.

Flatiron School alumni and Full Stack web developer.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store