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
bindimplementation - 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 —thisrefers tocounter. - When passing
counter.adddirectly to another function (operate), it loses context and throws an error. - Using
call()explicitly sets the context. - Using
bind()returns a new function withthispermanently 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.printwithobjBasthis, 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 objBPreserving 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
thiscontext 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()setsthistostore2, 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.logis passed intorateLimit()which returns a throttled version of the method.- Since
logrelies onthis.message,bind(logger)is required to preserve the context. - Without
bind(),this.messagewould beundefinedwhenlog()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.5sUsing 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, JohnArrow 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, AlicePreserving 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 thatthisalways 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