051: The `this` Keyword, call, apply, bind, and Function Borrowing
Outcomes
By the end of this lesson, you should be able to:
- explain that
thisis determined by how a normal function is called; - distinguish method calls, plain calls, constructor calls, and explicit binding;
- explain why arrow functions do not have their own
this; - use
call(),apply(), andbind()intentionally; - recognize function borrowing;
- avoid losing method context in callbacks and event handlers.
Mental Model: this Belongs to the Call Site
When you are working with a normal function, the most useful question is not where the function was written. Ask how this particular call is being made. The call site determines the function's this value.
const order = {
id: "ORD-101",
print() {
console.log(this.id);
},
};
order.print(); // "ORD-101"
For this call, the object immediately before the dot is the receiver, so that receiver becomes this. Keep that call-form model in mind as the examples become less direct.
Plain Function Calls
In strict mode, calling a normal function without a receiver gives it undefined as this:
"use strict";
function showThis() {
console.log(this);
}
showThis(); // undefined
Do not build code around the older sloppy-mode behavior, in which a plain call may fall back to the global object. Strict mode makes the missing receiver visible instead of silently supplying one, which is generally safer for debugging.
Method Calls
Calling a function through an object supplies that object as the receiver:
const branch = {
name: "Anna Nagar",
describe() {
return `Branch: ${this.name}`;
},
};
console.log(branch.describe());
The method does not permanently remember branch, though. Extracting the function changes the call site:
const describe = branch.describe;
describe(); // `this` is not `branch`
The function is now being called as a plain function, not as branch.describe(). Losing that receiver is one of the most common sources of this bugs, especially when a method is passed to another API as a callback.
Arrow Functions
Arrow functions do not create their own this. Instead, they capture this lexically from the surrounding scope. That makes them useful when a nested callback should keep the method's receiver:
const counter = {
count: 0,
start() {
setTimeout(() => {
this.count += 1;
console.log(this.count);
}, 100);
},
};
counter.start();
The arrow does not receive a new dynamic this from setTimeout; it uses the this already available inside start(). In this case, that is exactly what preserves the surrounding method context.
The same rule makes an arrow a poor choice for an object method when you expect the method call to provide a dynamic receiver:
const user = {
name: "Maya",
greet: () => {
console.log(this.name);
},
};
The arrow does not get user as its this. Its this comes from the scope in which the object literal was created, so user.greet() does not turn user into the arrow's receiver.
Constructor Calls
The new call form is another special case. JavaScript creates a new object and uses that object as this while the constructor runs:
function Product(name, price) {
this.name = name;
this.price = price;
}
const item = new Product("Tea", 20);
console.log(item.name);
Here, assignments through this initialize the newly created item. Modern code often uses class syntax for this kind of object construction, but understanding constructor calls makes the underlying object model and the meaning of this easier to follow.
Explicit Binding with call
Sometimes the receiver should be selected explicitly rather than inferred from the call form. call invokes the function immediately and supplies the chosen object as this:
function describe(prefix) {
return `${prefix}: ${this.name}`;
}
const product = { name: "Biryani" };
console.log(describe.call(product, "Product"));
call invokes immediately and accepts the function's arguments individually after the object used for this.
Explicit Binding with apply
apply has the same immediate invocation behavior, but it receives the function arguments as an array-like collection:
function total(a, b, c) {
return this.base + a + b + c;
}
const context = { base: 10 };
console.log(total.apply(context, [1, 2, 3]));
Use apply when the arguments are already grouped in an array-like value. For newly written code, spread syntax often makes the equivalent call form easier to read:
const values = [1, 2, 3];
total.call(context, ...values);
Both examples invoke total immediately with context as this; the difference is only how the arguments are supplied.
Permanent Binding with bind
Unlike call and apply, bind does not invoke the function at once. It returns a new function whose this is fixed for later calls:
const printer = {
prefix: "ORDER",
print(id) {
console.log(`${this.prefix}-${id}`);
},
};
const printOrder = printer.print.bind(printer);
printOrder(101);
This is useful when handing a method to another API. The returned callback can be called without the original dot expression and still use printer as its receiver.
Event Handler Context
Traditional DOM listener functions receive the current target as this:
button.addEventListener("click", function () {
console.log(this === button); // true
});
Production code is often clearer when it reads the event object explicitly instead:
button.addEventListener("click", (event) => {
console.log(event.currentTarget);
});
Relying on event.currentTarget usually makes the dependency more obvious. It also avoids making the callback's behavior depend on the special this convention used by the DOM listener API.
Function Borrowing
Function borrowing means using a method defined for one object with another compatible object. The method is reused, but the receiver is supplied by the call:
const formatter = {
fullName() {
return `${this.firstName} ${this.lastName}`;
},
};
const customer = {
firstName: "Anu",
lastName: "Raj",
};
console.log(formatter.fullName.call(customer));
fullName only requires that its this value provide firstName and lastName. The borrowed function does not care where it was originally stored; it cares about the call context supplied for this invocation.
Worked Example: Preserve a Class Method Callback
Class methods have the same call-site behavior as other normal functions. If a class method is passed directly as a callback, the callback API may call it without the instance as its receiver. Binding the method once gives the callback a stable identity and preserves the instance context:
class CartController {
constructor(button) {
this.button = button;
this.items = [];
this.handleAdd = this.handleAdd.bind(this);
this.button.addEventListener("click", this.handleAdd);
}
handleAdd() {
this.items.push({ id: crypto.randomUUID() });
console.log(this.items.length);
}
destroy() {
this.button.removeEventListener("click", this.handleAdd);
}
}
Binding once in the constructor creates one stable function reference. That same reference is used when the listener is added and when it is later removed, so destroy() can clean up the listener correctly.
An alternative is a class-field arrow in environments or toolchains that support the syntax you target:
class CartController {
handleAdd = () => {
this.items.push({ id: crypto.randomUUID() });
};
}
The class-field arrow captures the instance context for each instance. The choice between this approach and binding in the constructor should follow the syntax and conventions supported by the project.
Failure Example: Binding During Removal
Binding at both registration and removal time looks symmetrical, but it does not use the same function object twice:
button.addEventListener("click", controller.handleAdd.bind(controller));
button.removeEventListener("click", controller.handleAdd.bind(controller));
These two bind() calls create two different function objects. removeEventListener therefore cannot match the second object to the listener registered with the first one, and the listener is not removed.
Store the bound function once, then reuse that stored reference for both operations.
this Decision Table
| Call form | Typical this |
|---|---|
fn() | undefined in strict mode |
obj.fn() | obj |
fn.call(obj) | obj |
fn.apply(obj) | obj |
fn.bind(obj) | bound object when returned function runs |
new Fn() | newly created instance |
| arrow function | inherited lexical this |
This table is a useful first diagnostic, but inspect the actual call form and function kind when a real case is more complicated. In particular, an arrow ignores attempts to provide its own dynamic this because it has no own this binding.
Advanced Notes: Method Extraction, Partial Application, and API Design
When a this bug appears, it is worth asking whether the behavior really needs an implicit receiver at all. Compare an object method that reads state through this:
const cart = {
taxRate: 0.18,
totalWithTax(subtotal) {
return subtotal + subtotal * this.taxRate;
},
};
with a dependency-explicit function:
function totalWithTax(subtotal, taxRate) {
return subtotal + subtotal * taxRate;
}
The second form is easier to reuse and test because its dependencies appear in its parameter list. Use this when object-oriented, stateful behavior genuinely improves the model, not simply because a method happens to be available.
bind() can pre-fill arguments too
bind can fix arguments in addition to fixing this:
function multiply(a, b) {
return a * b;
}
const double = multiply.bind(null, 2);
console.log(double(5)); // 10
This is partial application: the first argument is supplied up front, and the returned function accepts the remaining argument. It can be convenient, although a closure is often clearer when there is no meaningful receiver to bind:
const double = (value) => multiply(2, value);
Method extraction test
Before passing a method as a callback, check whether it reads this. If it does, pass a bound function or an explicit wrapper rather than assuming the callback API will preserve the instance:
class Reporter {
constructor(prefix) {
this.prefix = prefix;
}
report(message) {
console.log(this.prefix, message);
}
}
const reporter = new Reporter("[APP]");
// Unsafe:
setTimeout(reporter.report, 0, "started");
// Safe:
setTimeout(reporter.report.bind(reporter), 0, "started");
The unsafe version extracts the method, so the timer invokes it without reporter as its receiver. The safe version supplies the instance explicitly. When a callback API passes its own arguments or receiver, an explicit wrapper can make that boundary clearer still.
Interview exercise
Explain why these two properties behave differently:
const object = {
value: 42,
normal() {
return this.value;
},
arrow: () => this.value,
};
Then explain why:
const normal = object.normal;
normal();
does not remember object.
The goal is not to memorize isolated slogans about this. The goal is to reason from the function kind and the call form: a normal method can receive a receiver from object.normal(), while the extracted function is being called plainly, and the arrow gets this lexically from its surrounding scope.
Mistakes and Debugging
Common mistakes include:
- treating
thisas lexical in normal functions; - assuming a method remembers its object after extraction;
- using arrow methods when dynamic
thisis required; - rebinding a function repeatedly and losing the original reference;
- using
thiswhen a plain parameter would make dependencies clearer.
When debugging, start at the call site rather than guessing from where the function was declared. Inspect the value of this and the stack that led to the call:
console.log("this =", this);
console.trace();
The observed value helps distinguish a plain call, an unexpected extraction, an explicit binding, or a callback API with its own calling convention.
Best Practices
- Prefer explicit parameters when context does not need to be dynamic.
- Use methods when behavior naturally belongs to an object.
- Use arrows to preserve surrounding lexical
this. - Bind once when stable callback identity matters.
- Prefer
event.currentTargetover implicit event-handlerthisfor clarity.
These practices reduce the amount of hidden call-site behavior a reader has to reconstruct, while still leaving this available where it represents genuine object state or a deliberate API contract.
Exercises
Core
Predict this in method, plain function, arrow, call, and new examples.
Practice
Repair this callback so that it can read the account's balance when the timer runs:
const account = {
balance: 100,
show() {
console.log(this.balance);
},
};
setTimeout(account.show, 0);
Professional Extension
Build a controller with:
mount();destroy();- a bound click handler;
- a test proving the listener is removed.
Recap
For normal functions, this is primarily a call-site concept. Method calls, plain calls, constructor calls, and explicit calls each establish context differently. call, apply, and bind let you control that context explicitly, with bind returning a function for later use. Arrow functions are different because they capture this from the surrounding lexical scope rather than receiving their own dynamic this.
