JS Fundamentals: The this Keyword, Part 2 (Controlling this Binding)
The five ways this is bound in JavaScript: default, implicit, explicit, new and arrow function binding, with their priority rules and common pitfalls.
Last updated: •3 min read
Previously
📚 The 5 ways this gets bound
| Type | Official name (Binding Type) | Binding rule | Example |
|---|---|---|---|
| 1️⃣ Default | Default Binding | Called directly → this = window / global | sayHi() |
| 2️⃣ Implicit | Implicit Binding | Called on an object → this = that object | obj.sayHi() |
| 3️⃣ Explicit | Explicit Binding | call / apply / bind set this explicitly | sayHi.call(obj) |
| 4️⃣ new | New Binding | Inside a constructor → this is the newly created object | new Person() |
| 5️⃣ Arrow | Lexical Binding | An arrow function is permanently bound to the scope where it was defined | () => this.name |
The previous article covered 1 and 2. This one covers the remaining bindings.
3️⃣ Explicit Binding
| Method | Runs immediately | Arguments | Purpose |
|---|---|---|---|
call | ✅ | Passed one by one | Calls immediately, changing this |
apply | ✅ | Passed as an array | Calls immediately, good when the arguments vary |
bind | ❌ | Passed one by one | Does not run; returns a new function |
const person = {
name: "Alice",
greet: function (greeting, punctuation) {
console.log(`${greeting}, I'm ${this.name}${punctuation}`);
},
};
const otherPerson = {
name: "Bob",
};
// call: runs immediately, arguments passed one by one
person.greet.call(otherPerson, "Hello", "!");
// → Hello, I'm Bob!
// apply: runs immediately, arguments passed as an array
person.greet.apply(otherPerson, ["Hi", "!!"]);
// → Hi, I'm Bob!!
// bind: does not run, returns a new bound function
const boundGreet = person.greet.bind(otherPerson, "Hey", "!!");
boundGreet();
// → Hey, I'm Bob!!
In short, bind defines a new function that differs from the original only in that this is fixed to a particular object.
call and apply both set this at the moment the function is called. The difference:
calltakes the arguments one by oneapplytakes an array of arguments
A scenario for bind:
function Button(callback) {
// simulate a click
callback();
// callback is called with default binding here; without bind, this points to window
}
const app = {
name: "MyApp",
clickHandler() {
console.log(`${this.name} was clicked`);
},
sayHello() {
console.log(`hello ${this.name}!`)
}
};
Button(app.clickHandler.bind(app)); // without bind, this would be undefined
setTimeout(app.sayHello.bind(app),1000); // the callback needs bind for this
When you pass a function as an argument (a callback), you need to bind it. Even if the function was written with its owning object in front of it, once it is passed along, this is lost.
A scenario for call and apply
function printFullName(preMessage) {
console.log(`${preMessage} ${this.firstName} ${this.lastName}`);
}
const user1 = {
firstName: "Ada",
lastName: "Lovelace",
};
const user2 = {
firstName: "Pizza",
lastName: "6inch",
}
printFullName.call(user1,"Hi"); // → Hi Ada Lovelace
printFullName.apply(user2,["Hi"]) // -> Hi Pizza 6inch
When calling third-party functions or tools, use call / apply when you need to change this according to the situation at the moment of the call.
4️⃣ New Binding
When you call a function with the new keyword, that function is treated as a constructor, and this is bound to the newly created object itself.
function Person(name) {
this.name = name; // with new, this is bound to user
}
const user = new Person("Ada");
console.log(user.name); // Ada
const user2 = Person("Ada"); // without new
console.log(user2); // undefined
console.log(window.name); // "Ada" (bound to window in non-strict mode)
5️⃣ Lexical Binding (arrow functions)
One of the biggest characteristics of arrow functions is this:
They don't have their own this. Instead they inherit the this of the enclosing scope where they were defined (static binding).
This is different from regular functions. For a regular function, this is decided when it runs, whereas for an arrow function, this is decided when it is defined.
- Because of this I often get caught out when solving coding problems 😭
- I'm so used to arrow functions that I had no idea how they differ from traditional functions 😂
const obj = {
name: "Alice",
greet: function () {
// sayHi is an arrow function
// this is bound to obj at the point of declaration
const sayHi = () => {
console.log(`Hi, I'm ${this.name}`);
};
sayHi(); // bound to this even without obj. in front of it
},
};
obj.greet(); // Hi, I'm Alice
A traditional function loses this:
const obj = {
name: "Bob",
greet: function () {
// traditional function
const sayHi = function () {
console.log(`Hi, I'm ${this.name}`);
};
sayHi(); // default binding here, this === window
},
};
obj.greet(); // Hi, I'm undefined
In practice
Keeping this consistent in event handlers:
class Button {
constructor(label) {
this.label = label;
this.handleClick = () => {
console.log(`Button: ${this.label}`);
};
}
}
const btn = new Button("Submit");
document.querySelector("button").addEventListener("click", btn.handleClick);
If an arrow function were not used here, this would point to the button DOM element, causing a bug.
Binding priority
There are five kinds of binding in total. When two or more of them conflict, the priority is:
Default < Implicit < Explicit < new ≈ Arrow (mutually exclusive, highest priority)
A constructor has to be a traditional function; in other words an arrow function can't be called with new, so the two never conflict.
- Using
bind,callorapplyon an arrow function or on anewcall has no effect onthis - Using
bind,callorapplyonobj.functionrebindsthis - With no binding at all,
thispoints towindow
Summary
Finally, here is the summary table again:
📚 The 5 ways this gets bound
| Type | Official name (Binding Type) | Binding rule | Example |
|---|---|---|---|
| 1️⃣ Default | Default Binding | Called directly → this = window / global | sayHi() |
| 2️⃣ Implicit | Implicit Binding | Called on an object → this = that object | obj.sayHi() |
| 3️⃣ Explicit | Explicit Binding | call / apply / bind set this explicitly | sayHi.call(obj) |
| 4️⃣ new | New Binding | Inside a constructor → this is the newly created object | new Person() |
| 5️⃣ Arrow | Lexical Binding | An arrow function is permanently bound to the scope where it was defined | () => this.name |
These notes drew on: kuro's Blog, ExplainThis, and ChatGPT.