051: The `this` Keyword, call, apply, bind, and Function Borrowing
Outcomes
By the end of this lesson, you can:
- 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
For normal functions, do not ask only "where was this function written?" Ask: how was it called?
const order = {
id: "ORD-101",
print() {
console.log(this.id);
},
};
order.print(); // "ORD-101"
The receiver before the dot becomes this for that call.
Plain Function Calls
In strict mode:
"use strict";
function showThis() {
console.log(this);
}
showThis(); // undefined
Do not depend on the older sloppy-mode behavior where this may fall back to the global object.
Method Calls
const branch = {
name: "Anna Nagar",
describe() {
return `Branch: ${this.name}`;
},
};
console.log(branch.describe());
But extracting the method changes the call site:
const describe = branch.describe;
describe(); // `this` is not `branch`
This is one of the most common this bugs.
Arrow Functions
Arrow functions capture this lexically from the surrounding scope.
const counter = {
count: 0,
start() {
setTimeout(() => {
this.count += 1;
console.log(this.count);
}, 100);
},
};
counter.start();
The arrow is useful because it preserves the surrounding method's this.
Do not use an arrow as an object method when you expect dynamic method this:
const user = {
name: "Maya",
greet: () => {
console.log(this.name);
},
};
The arrow does not get user as its this.
Constructor Calls
With new, JavaScript creates a new object and binds it as this.
function Product(name, price) {
this.name = name;
this.price = price;
}
const item = new Product("Tea", 20);
console.log(item.name);
Modern code often uses class syntax, but understanding constructor calls helps explain the object model.
Explicit Binding with call
function describe(prefix) {
return `${prefix}: ${this.name}`;
}
const product = { name: "Biryani" };
console.log(describe.call(product, "Product"));
call invokes immediately and accepts arguments individually.
Explicit Binding with apply
function total(a, b, c) {
return this.base + a + b + c;
}
const context = { base: 10 };
console.log(total.apply(context, [1, 2, 3]));
apply invokes immediately and takes arguments as an array-like collection.
Spread syntax often makes call more readable today:
const values = [1, 2, 3];
total.call(context, ...values);
Permanent Binding with bind
bind returns a new function.
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.
Event Handler Context
Traditional DOM listener functions receive the current target as this:
button.addEventListener("click", function () {
console.log(this === button); // true
});
But production code is often clearer when it uses the event object explicitly:
button.addEventListener("click", (event) => {
console.log(event.currentTarget);
});
Relying on event.currentTarget usually makes the dependency more obvious.
Function Borrowing
A method can be used with another compatible object.
const formatter = {
fullName() {
return `${this.firstName} ${this.lastName}`;
},
};
const customer = {
firstName: "Anu",
lastName: "Raj",
};
console.log(formatter.fullName.call(customer));
The borrowed function does not care where it was originally stored; it cares about the call context.
Worked Example: Preserve a Class Method Callback
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 a stable function reference that can later be removed.
An alternative is a class-field arrow in environments/toolchains that support the syntax you target:
class CartController {
handleAdd = () => {
this.items.push({ id: crypto.randomUUID() });
};
}
Failure Example: Binding During Removal
button.addEventListener("click", controller.handleAdd.bind(controller));
button.removeEventListener("click", controller.handleAdd.bind(controller));
These two bind() calls create two different function objects. The listener is not removed.
Store the bound function once.
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 |
Advanced Notes: Method Extraction, Partial Application, and API Design
A useful way to avoid this bugs is to ask whether the behavior really needs an implicit receiver.
Compare:
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 is easier to reuse and test because its dependencies are visible. Use this when object-oriented stateful behavior genuinely improves the model, not merely because methods are available.
bind() can pre-fill arguments too
function multiply(a, b) {
return a * b;
}
const double = multiply.bind(null, 2);
console.log(double(5)); // 10
This is partial application. It can be convenient, though closures are often clearer:
const double = (value) => multiply(2, value);
Method extraction test
Before passing a method as a callback, ask whether it reads this.
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");
When a callback API passes its own arguments or receiver, explicit wrappers can make the boundary clearer.
Interview exercise
Explain why these differ:
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 slogans. The goal is to reason from the call form.
Mistakes and Debugging
- 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.
Debug the call site:
console.log("this =", this);
console.trace();
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.
Exercises
Core
Predict this in method, plain function, arrow, call, and new examples.
Practice
Repair:
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
this is primarily a call-site concept for normal functions. call, apply, and bind let you control it explicitly. Arrow functions are different because they capture the surrounding this.
