Understanding call, apply, and bind in JavaScript

In Brief
For an ordinary JavaScript function, call() and apply() invoke it with a supplied receiver, whilst bind() creates a function with a receiver and optional arguments preset. Arrow functions keep their own lexical this, so these methods cannot replace it.
For an ordinary function, JavaScript's this depends on how it is called. Arrow functions are different: they keep the this of the surrounding scope where they were created.
That distinction matters in real code reviews. You might see call, apply or bind in legacy modules, event handlers, callbacks, decorators, functional utilities, tests, mocks, or third‑party library integrations. Modern JavaScript has reduced the need for manual binding in many day‑to‑day cases, but it has not removed the need to understand context.
The mistake to avoid is using bind as a quick patch without asking why the context was lost. Sometimes binding is exactly the right fix. Sometimes it hides a muddled component boundary, an extracted method being passed around too freely or a callback that should be an arrow function.
What call, apply and bind do
These methods let us supply arguments and, for functions with their own dynamic this, a receiver. An arrow still accepts the arguments, but ignores the supplied receiver.
The difference is when the function runs and how its arguments are supplied.
call
call invokes the function immediately. You pass the this value first, then each argument separately.
function greet(greeting, name) {
console.log(`${greeting}, ${name}! My context is:`, this);
}
greet.call({ role: 'developer' }, 'Hello', 'Maddie');
// Output: Hello, Maddie! My context is: { role: 'developer' }apply
apply also invokes the function immediately, but it expects the arguments as an array or array‑like object.
const numbers = [5, 10, 15];
const max = Math.max.apply(null, numbers);
console.log(max); // Output: 15bind
bind() returns a new function without calling the original. For an ordinary call to that bound function, its receiver is fixed. Constructing it with new has a different rule, covered below.
const user = { name: 'Sophie' };
function introduce() {
console.log(`My name is ${this.name}`);
}
const boundIntroduce = introduce.bind(user);
boundIntroduce(); // Output: My name is SophieThat makes bind useful when you need a function reference, not an immediate result.
call in practice
Use call when you want to run a function now and provide the context directly.
It is useful for borrowed methods, array‑like objects and small cases where explicit invocation is clearer than wrapping the function in another function.
Syntax of call
functionName.call(thisArg, arg1, arg2, ...);thisArg: The value to use asthisinside the function.arg1, arg2, ...: The arguments passed to the function one by one.
Borrowing methods with call
Borrowing a method is the classic example. The method belongs to one object or prototype, but you want to run it against another value.
const person = {
firstName: 'John',
lastName: 'Doe',
fullName: function () {
return `${this.firstName} ${this.lastName}`;
},
};
const anotherPerson = { firstName: 'Jane', lastName: 'Smith' };
// Borrowing fullName method for anotherPerson
console.log(person.fullName.call(anotherPerson)); // Output: "Jane Smith"This pattern appears less often in modern application code than it used to, but it still appears around array‑like values, DOM APIs, and old utility code.
Passing arguments with call
When the arguments are already known, call keeps the invocation direct.
function greet(greeting, punctuation) {
console.log(`${greeting}, ${this.name}${punctuation}`);
}
const user = { name: 'Ellie' };
// Invoking greet with a custom `this` and arguments
greet.call(user, 'Hello', '!'); // Output: "Hello, Ellie!"What to watch with call
- A strict function preserves the supplied
thisArgexactly, includingnull,undefinedand primitive values. - An ordinary non‑strict function replaces
nullorundefinedwith the global object and wraps other primitive receivers. That can hide a missing receiver rather than fixing it. - If the argument list is already an array,
applyor spread syntax may be clearer.
apply in practice
apply is close to call, but the argument list is supplied as an array‑like value.
That makes it useful when a function receives or constructs arguments dynamically.
Syntax of apply
functionName.apply(thisArg, [arg1, arg2, ...]);thisArg: The value to use asthisinside the function.[arg1, arg2, ...]: An array or array‑like object containing the arguments.
Borrowing methods with apply
apply can borrow methods in the same way as call. The difference is the argument shape.
const numbers = [5, 10, 15, 20];
const maxNumber = Math.max.apply(null, numbers);
console.log(maxNumber); // Output: 20In this example, apply makes it easy to pass the array of numbers to Math.max.
Another apply example
function introduce(greeting, punctuation) {
console.log(`${greeting}, my name is ${this.name}${punctuation}`);
}
const person = { name: 'Ellie' };
const args = ['Hello', '!'];
introduce.apply(person, args); // Output: "Hello, my name is Ellie!"What to watch with apply
- Use
applywhen the arguments are already array‑like. - For ordinary arrays, spread syntax is often easier to read in modern JavaScript.
- Do not use
applyjust to look clever. Explicit code usually wins in production reviews.
bind in practice
bind() fixes the receiver for ordinary calls to a function that has dynamic this. It can also preset arguments, including for an arrow whose lexical this will remain unchanged.
That is useful when a function will be called later by something else, such as an event listener, timer, callback, or library API.
Syntax of bind
const boundFunction = functionName.bind(thisArg, arg1, arg2, ...);thisArg: The value to use asthisinside the new function.arg1, arg2, ...: Optional arguments that are pre‑set when the new function is called.
Preserving context with bind
const user = {
name: 'Maddie',
greet: function () {
console.log(`Hello, ${this.name}!`);
},
};
const greetUser = user.greet.bind(user);
// Even if `greetUser` is called in a different context, `this` is preserved
greetUser(); // Output: "Hello, Maddie!"This is the use case most developers recognise: a method is passed around, and you still need it to run with the original object as this.
Pre‑setting arguments with bind
bind can also pre‑set arguments. That creates a specialised function from a more general one.
function multiply(a, b) {
return a * b;
}
const double = multiply.bind(null, 2); // `a` is pre-set to 2
console.log(double(5)); // Output: 10In this example, bind creates double by fixing the first argument of multiply to 2. Calling double(5) then calls multiply(2, 5).
What to watch with bind
- A bound function is a new function reference.
- Binding inside render paths or repeated setup code can create avoidable churn.
- If an arrow function would make the ownership clearer, prefer the clearer option.
- If the target is constructable, calling the bound function with
newignores its bound receiver and uses the newly created instance. Preset arguments are still passed through; binding an arrow does not make it constructable.
When to Use Each One
The short version is straightforward.
Use call for immediate invocation with listed arguments
const person = {
name: 'John',
};
function introduce(age) {
console.log(`Hi, I'm ${this.name} and I'm ${age} years old.`);
}
introduce.call(person, 30);
// Output: Hi, I'm John and I'm 30 years old.call is useful when you want the invocation to be explicit and the argument list is known.
Use apply for immediate invocation with array‑like arguments
const numbers = [5, 1, 7, 3];
const maxNumber = Math.max.apply(null, numbers);
console.log(maxNumber); // Output: 7apply is useful when the arguments already exist as a list. In modern code, compare it with spread syntax before choosing it.
Use bind for delayed invocation
const printer = {
prefix: 'Message:',
};
function printMessage(message) {
console.log(`${this.prefix} ${message}`);
}
const boundPrinter = printMessage.bind(printer);
boundPrinter('Hello, World!');
// Output: Message: Hello, World!bind is useful when another part of the system will call the function later and you need the context or initial arguments to stay fixed.
The Production Caveat
Arrow functions can reduce the need for manual binding when a callback should use the surrounding this. They are useful for that purpose, rather than a replacement for every method or constructor.
The caveat is that production code still contains older patterns, library callbacks and framework integration points. Understanding call, apply and bind helps you read that code without guessing.
It also helps you avoid the lazy fix. If this changes unexpectedly, first ask how the function is being called. The related article on why this changes in JavaScript event handlers and methods covers that failure mode directly.
Use these methods when they make invocation clearer. Avoid them when they make the code look more advanced than the problem deserves.