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!
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!
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!
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!
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!
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 class
es. 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 emptyobject
. 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 itsthis
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 returnthis
.
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
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
In
strict mode
, the default behaviour is to leavethis
undefined
, which can causeTypeError
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?
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)
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.