call, bind, and apply in JavaScript
Understanding call
, bind
, and apply
is essential for mastering how JavaScript handles function context (this
). These tools allow you to control
the execution context of functions, borrow behavior across objects, and preserve method bindings in callbacks and asynchronous code.
This guide includes:
- Differences between
call
,apply
, andbind
- Use cases for method borrowing, decorators, and delayed execution
- A custom
bind
implementation - Behavior of arrow functions with
this
Losing and Rebinding Context
This example demonstrates what happens when a method loses its context (this
) and how to recover it using call()
and bind()
:
- Calling
counter.add(1)
works as expected —this
refers tocounter
. - When passing
counter.add
directly to another function (operate
), it loses context and throws an error. - Using
call()
explicitly sets the context. - Using
bind()
returns a new function withthis
permanently bound tocounter
.
const counter = {
value: 10,
add(num) {
this.value += num;
return this.value;
},
};
function operate(method, num) {
return method(num);
}
function operateWithCall(method, num) {
return method.call(counter, num);
}
console.log(counter.add(1)); // 11
console.log(operateWithCall(counter.add, 1)); // 12
console.log(operate(counter.add.bind(counter), 1)); // 13
try {
console.log(operate(counter.add, 1));
} catch (e) {
console.error(e.message);
// Cannot read properties of undefined (reading 'value')
}
Using call
and apply
to Change Context
This example shows how call()
and apply()
can be used to invoke a method with a different this
context:
objA.print.call(objB)
executesobjA.print
withobjB
asthis
, so it logs"I am from objB"
.apply()
works similarly, but takes arguments as an array.
const objA = {
data: "I am from objA",
print() {
console.log(this.data);
},
};
const objB = {
data: "I am from objB",
};
objA.print.call(objB); // I am from objB
objA.print.apply(objB); // I am from objB
Preserving Method Context with setTimeout
This example shows how to preserve the this
context when calling a method asynchronously using setTimeout
.
The delayMethod
function returns a wrapper that defers method execution by 1 second while ensuring that this
still refers to the original object.
user.greet()
usesthis.name
, so it’s crucial to call it with the correct context.- Wrapping it with
obj[methodName](...args)
inside the timeout maintains the correctthis
.
function delayMethod(obj, methodName) {
return function (...args) {
setTimeout(() => obj[methodName](...args), 1000);
};
}
const user = {
name: "Alice",
greet(greeting) {
console.log(`${greeting}, ${this.name}!`);
},
};
const delayedGreet = delayMethod(user, "greet");
delayedGreet("Hello"); // After 1 second: "Hello, Alice!"
Custom bind()
Implementation
This example demonstrates how to implement a simplified version of Function.prototype.bind()
.
The customBind
function:
- Stores a reference to the original function.
- Returns a new function that, when called, invokes the original with a fixed
this
context and any partially applied arguments.
This mimics native bind()
behavior, including partial application of arguments.
Function.prototype.customBind = function (context, ...args) {
const originalFunction = this;
return function (...newArgs) {
return originalFunction.apply(context, [...args, ...newArgs]);
};
};
function greet(greeting, punctuation) {
console.log(`${greeting}, ${this.name}${punctuation}`);
}
const person = { name: "John" };
const boundGreet = greet.customBind(person, "Hello");
boundGreet("!"); // "Hello, John!"
Sharing Methods Across Objects with call()
In this example, the calculateTotal
method from store1
is reused for store2
by using Function.prototype.call()
.
call()
setsthis
tostore2
, so the method computes tax usingstore2.taxRate
.- As an alternative, the method is directly assigned to
store2
, allowing normal invocation syntax.
const store1 = {
taxRate: 0.1,
calculateTotal(price, quantity) {
const subtotal = price * quantity;
return subtotal + subtotal * this.taxRate;
},
};
const store2 = {
taxRate: 0.2,
};
const price = 50;
const quantity = 2;
// Calculate total for `store2` using the method from `store1`
const total = store1.calculateTotal.call(store2, price, quantity);
console.log(total); // 120 (price * quantity + tax)
// 2 solution
store2.calculateTotal = store1.calculateTotal;
console.log(store2.calculateTotal(price, quantity));
bind()
for Context in Rate-Limited Function
This example demonstrates how to use bind()
to ensure the correct this
context when passing an object’s method to a wrapper function.
logger.log
is passed intorateLimit()
which returns a throttled version of the method.- Since
log
relies onthis.message
,bind(logger)
is required to preserve the context. - Without
bind()
,this.message
would beundefined
whenlog()
is called inside the throttled wrapper.
function rateLimit(fn, ms) {
let lastCall = 0;
return function (...args) {
const now = Date.now();
if (now - lastCall < ms) return;
lastCall = now;
return fn(...args);
};
}
const logger = {
message: "Rate limited log",
log() {
console.log(this.message);
},
};
const rateLimitedLog = rateLimit(logger.log.bind(logger), 1000);
rateLimitedLog(); // Rate limited log
rateLimitedLog();
setTimeout(rateLimitedLog, 1500);
// Logs "Rate limited log" after 1.5s
Using bind()
to Preserve this
In this example, greet()
is a regular function that relies on this.name
. When it’s not called in the context of an object, this
would be undefined
.
To ensure the correct context (person
), we create a bound version of the function using bind(person)
.
This guarantees that this.name
inside greet()
always refers to "John"
, even if called independently.
const person = {
name: "John",
};
function greet() {
console.log(`Hello, ${this.name}`);
}
// Create a bound function
const boundGreet = greet.bind(person);
boundGreet(); // Hello, John
Arrow Functions Ignore call
In this example, the arrow
function is defined inside the greet()
method using an arrow function.
Arrow functions do not have their own this
— they inherit this
from their enclosing lexical context.
So even when using call()
with a different this
value ({ name: "Bob" }
), the arrow function still uses
this.name
from the outer greet()
context, which is user
.
Thus, the output is: Hello, Alice
.
const user = {
name: "Alice",
greet() {
const arrow = () => console.log(`Hello, ${this.name}`);
arrow.call({ name: "Bob" }); // Arrow functions ignore `call`
},
};
user.greet(); // Hello, Alice
Preserving this
in Callbacks
When passing a method like user.logName
to another function (e.g. executeCallback
), the this
context is lost unless it is explicitly preserved.
bind(user)
ensures thatthis
always refers touser
.- An arrow function
() => user.logName()
also preserves the context by calling the method directly onuser
.
Both approaches ensure the correct this
context, so the output is: Alice
.
const user = {
name: "Alice",
logName() {
console.log(this.name);
},
};
function executeCallback(callback) {
callback();
}
// Preserve context with `bind`
executeCallback(user.logName.bind(user)); // Alice
executeCallback(() => user.logName()); // Alice