DEV Community

Cover image for 🚀How JavaScript Works (Part 8)? Binding
Sam Abaasi
Sam Abaasi

Posted on

🚀How JavaScript Works (Part 8)? Binding

In JavaScript, the this keyword is a powerful, yet sometimes perplexing, element. Understanding how it behaves is crucial for writing effective and maintainable code. We will discuss four essential ways to invoke a function and how each method affects the binding of this. We will explore these methods, understand their implications, and provide code examples.

Table of Contents

Function Invocation and the "this" Keyword
Implicit Binding
Explicit Binding with .call and .apply
The Challenge of Losing this Binding
Hard Binding
Striking a Balance
The new Keyword
Default Binding
Binding Precedence
Conclusion
Sources

Function Invocation and the "this" Keyword

When you invoke a function in JavaScript, the behavior of the this keyword depends on the context in which the function is called. Let's explore the four primary methods of function invocation.

1. Implicit Binding

Implicit binding is one of the most common and intuitive ways to handle the this keyword. It relies on the call site, which is the object used to invoke a function.

const workshop = {
    topic: "JavaScript",
    ask: function () {
        console.log(`Welcome to the ${this.topic} workshop!`);
    },
};

workshop.ask(); // Outputs: Welcome to the JavaScript workshop!
Enter fullscreen mode Exit fullscreen mode

In this example, the this keyword within the ask function refers to the workshop object. Implicit binding associates this with the object that triggers the function.

Implicit binding is a powerful mechanism for sharing behavior among different contexts. You can define a function once and use it across multiple objects, allowing flexibility and code reuse.

2. Explicit Binding with .call and .apply

Explicit binding provides control over the this context of a function using the .call and .apply methods. These methods take the desired this context as their first argument.

const workshop1 = { topic: "JavaScript" };
const workshop2 = { topic: "React" };

function ask() {
    console.log(`Welcome to the ${this.topic} workshop!`);
}

ask.call(workshop1); // Outputs: Welcome to the JavaScript workshop!
ask.call(workshop2); // Outputs: Welcome to the React workshop!
Enter fullscreen mode Exit fullscreen mode

With explicit binding, you explicitly specify the this context, allowing you to share functions across different objects but with precise control.

Implicit Sharing of Behavior

Implicit binding enables developers to share behavior across different contexts by referencing a single function. For example:

const workshop1 = { topic: "JavaScript" };
const workshop2 = { topic: "React" };

function ask() {
    console.log(`Welcome to the ${this.topic} workshop!`);
}

workshop1.ask = ask;
workshop2.ask = ask;

workshop1.ask(); // Outputs: Welcome to the JavaScript workshop!
workshop2.ask(); // Outputs: Welcome to the React workshop!
Enter fullscreen mode Exit fullscreen mode

In this scenario, you share a single ask function between workshop1 and workshop2, invoking it with distinct contexts.

The Challenge of Losing this Binding

One challenge in JavaScript is losing the this binding when you pass a function around. Consider the following example:

function delayedAsk() {
    setTimeout(function () {
        console.log(`Welcome to the ${this.topic} workshop!`);
    }, 1000);
}

const workshop = { topic: "JavaScript" };

delayedAsk(); // Outputs: Welcome to undefined workshop!
Enter fullscreen mode Exit fullscreen mode

The this context inside the setTimeout callback function is not bound to the workshop object. Instead, it defaults to the global context, resulting in undefined.

Introducing Hard Binding

To address the issue of losing the this binding, developers often use a technique called "hard binding". This technique ensures that a function's this context remains fixed, no matter how it is called. The .bind method allows you to create a new function with a specific this context:

function delayedAsk() {
    setTimeout(
        function () {
            console.log(`Welcome to the ${this.topic} workshop!`);
        }.bind(this), // Hard binding to the workshop object
        1000
    );
}

const workshop = { topic: "JavaScript" };

delayedAsk(); // Outputs: Welcome to the JavaScript workshop!
Enter fullscreen mode Exit fullscreen mode

By using .bind(this), you force the this context within the setTimeout callback to always reference the workshop object.

This approach provides predictability but sacrifices some of the flexibility inherent in JavaScript.

Hard binding is valuable when you need consistent behaviour across function invocations, but it should be used thoughtfully to balance predictability and flexibility.

Striking a Balance

The choice between flexible and predictable this binding is not accidental; it is intentional. Implicit binding offers flexibility, allowing you to share behavior among different contexts. However, there are scenarios where predictability is essential. The key is to strike a balance:

Use implicit or explicit binding when flexibility is required and different contexts are beneficial.
Utilize hard binding when you need predictability, ensuring that the this context remains fixed.
However, overusing hard binding may lead to a less maintainable and rigid codebase. Strive for a balance that aligns with your project's requirements, offering the right mix of flexibility and predictability for your specific use cases.

3. The new Keyword

The new keyword is commonly misunderstood as a class instantiation operator. However, it's not inherently tied to classes. Its true role is to invoke a function with specific behaviours. When you use new with a function, it carries out four distinct tasks:

  • Creating a New Empty Object: new instantly generates a new empty object. This is the object that the function will operate on.

  • Linking to Another Object: The new keyword connects the newly created object to another object (to the prototype).

  • Invoking the Function: The function specified after new is called. However, the function is executed with its this keyword bound to the new object, not the linked one. This is a crucial distinction.

  • Handling Return Values: After executing the function, if it doesn't return its own object explicitly, the new keyword assumes that the function intends to return this.

Deconstructing the new Keyword

The new keyword is deceptively simple, as it essentially hijacks the constructor function to accomplish these four critical tasks. The function's implementation is secondary to the actions carried out by new. Even if you use new with an entirely empty function, it still executes these four tasks. In essence, it's the new keyword that does the heavy lifting, not the function itself.

Example of Using the new Keyword

Here's an illustrative example of how the new keyword works with a constructor function:

function Workshop(topic) {
    this.topic = topic;
}

const jsWorkshop = new Workshop("JavaScript");

console.log(jsWorkshop.topic); // Outputs: JavaScript
Enter fullscreen mode Exit fullscreen mode

In this example, the new keyword creates a new object, links it to Workshop, invokes the Workshop function with the this context set to the new object, and implicitly returns the new object.

4. Default Binding

The fourth way to bind this is the fallback, called default binding. If none of the other three methods apply, the this keyword can default to either the global object (in non-strict mode) or undefined (in strict mode).

var teacher = "Kyle";
function ask(question) {
    console.log(this.teacher,question);
}
function askAgain(question) {
    "use strict";
    console.log(this.teacher,question);
}
ask("Whats the non-strict-mode default?");
// Kyle Whats the non-strict-mode default?

askAgain("Whats the strict-mode default?");
// TypeError
Enter fullscreen mode Exit fullscreen mode

In strict mode, the default behaviour is to leave this undefined, which can cause TypeError if you try to access properties on it.

Binding Precedence

In JavaScript, understanding the binding precedence of the this keyword is essential to determine what it references when a function is invoked. Whether you're working with complex call sites or trying to unravel the intricacies of a particular code snippet, knowing the order of precedence for binding rules can be incredibly helpful. Consider this example:

var workshop = {
    teacher: "kyle",
    ask: function ask(question) {
     console.log(this.teacher, question);
    },
};
new (workshop.ask.bind(workshop))("What does this do?");
// undefined What does this do?
Enter fullscreen mode Exit fullscreen mode

A straightforward guide helps establish this order of precedence when determining the value of this in a function. Let's break down these rules step by step:

1. New Keyword
The first rule of precedence asks one simple question: Was the function called with the new keyword? If the answer is yes, the this keyword will point to the newly created object. This rule takes precedence over all others, making the new keyword a powerful way to explicitly define the context for a function.

2. Call or Apply
The second rule comes into play when the function is invoked using call or apply. It's important to note that bind is a sub-rule of this second rule, as it internally relies on apply. When call or apply is used, the context object specified as the first argument becomes the value of this. This rule allows you to directly control the context of a function.

3. Context Object
The third rule considers whether the function is called on a context object, like workshop.ask in the example. If the function is invoked within a specific object, the this keyword will reference that object. This rule provides a simple and intuitive way to manage context within object methods.

4. Default Binding
When none of the previous three rules apply, the fourth and final rule comes into effect. By default, the this keyword references the global object, except in strict mode. In strict mode, it defaults to undefined. This rule serves as a fallback when other binding rules don't match.

Conclusion

Understanding the behaviour of the this keyword and the four ways to invoke functions in JavaScript is fundamental to writing efficient and maintainable code. By choosing the right method for your specific use case, you can harness the full power of JavaScript's dynamic and flexible nature while ensuring predictable behaviour where needed.

Ultimately, JavaScript offers a variety of tools for function invocation and this binding, allowing you to tailor your code to the demands of your projects. The key is to strike the right balance between predictability and flexibility, enabling you to create robust and versatile JavaScript applications.

But let's reset our minds back. The question that we set out to ask is, if I have a this-aware function, how do I know what its this keyword points at? And our strong temptation is, we want to assume that we can just answer that by looking at the function. What we've now seen is that there's no way to look at the function to answer that question. You have to look at the call site. You have to look at how the function's being called. Because every time it gets called, the how of the call controls what the this keyword will point at.

Sources

Kyle Simpson's "You Don't Know JS"
MDN Web Docs - The Mozilla Developer Network (MDN)

Top comments (1)

Collapse
 
deepa_khanna_0a3b4b91d9f3 profile image
Deepa Khanna


function delayedAsk() {
setTimeout(
function () {
console.log(`Welcome to the ${this.topic} workshop!`);
}.bind(this), // Hard binding to the workshop object
1000
);
}

this will also print

Welcome to the undefined workshop!

because this is still part of global execution.
Instead trying

function delayedAsk() {
setTimeout(
function () {
console.log(`Welcome to the ${this.topic} workshop!`);
}.bind(workshop), // Hard binding to the workshop object
1000
);
}

works well.