The one-line answer: call and apply run the function right now with the this you choose, and bind doesn’t run it at all. bind hands you a new function whose this is locked, so you can call it later. Between call and apply, the only difference is how you pass the remaining arguments: call takes them one by one, apply takes a single array (or array-like object).
A memory trick that interviewers have heard a thousand times but still accept: apply takes an array, call takes commas.
The three methods side by side
call |
apply |
bind |
|
|---|---|---|---|
| Runs the function | Immediately | Immediately | No, returns a new function |
| Arguments | fn.call(thisArg, a, b) |
fn.apply(thisArg, [a, b]) |
fn.bind(thisArg, a) then bound(b) |
| Return value | Whatever fn returns |
Whatever fn returns |
A bound function |
| Can partially apply arguments | No | No | Yes, leading arguments are stored |
| Effect on later calls | None | None | this stays fixed, even under call or apply |
| Typical modern use | Borrowing a method | Rare now; spread replaced most uses | Callbacks and event handlers that need a specific this |
All three live on Function.prototype, so every regular function has them. They exist because this in JavaScript is decided by how a function is called, not where it was written. If that rule feels shaky, the full set of binding rules is covered in the this keyword guide; this article focuses on the three tools for setting it explicitly.
One function, three ways to call it
Here’s a function that reads this.amount, called through each method. Everything below runs as an ES module in Node 22.
function formatPrice(currency, digits) {
return `${currency}${this.amount.toFixed(digits)}`;
}
const order = { amount: 42.5 };
console.log(formatPrice.call(order, '£', 2));
console.log(formatPrice.apply(order, ['$', 0]));
const asEuro = formatPrice.bind(order, '€');
console.log(asEuro(1));
console.log(typeof asEuro, asEuro.name, asEuro.length);
const other = { amount: 1 };
console.log(asEuro.call(other, 2));Output:
£42.50
$43
€42.5
function bound formatPrice 1
€42.50Walk through what each line proves. call passed '£' and 2 as separate arguments. apply passed the same shape of data as one array. bind stored both order and the first argument '€', so the returned function only needed digits. That’s partial application, and it’s why asEuro.length is 1 rather than 2: the bound function reports how many parameters are still expected.
The last line is the one people get wrong. asEuro.call(other, 2) still printed 42.50, not 1.00. Once a function is bound, call, apply and even a second bind can’t change its this. The new argument 2 was accepted (arguments still flow through), but the this you passed was ignored. The only thing that overrides a bound this is calling the bound function with new, which builds a fresh object instead.
Where each one shows up in real code
bind: keeping a method attached to its object
The classic bug is passing a method somewhere as a bare function reference:
class Counter {
count = 0;
increment() { this.count++; return this.count; }
}
const c = new Counter();
const inc = c.increment;
try { inc(); } catch (e) { console.log(e.constructor.name + ': ' + e.message); }
const bound = c.increment.bind(c);
bound(); bound();
console.log(c.count);TypeError: Cannot read properties of undefined (reading 'count')
2inc() is a plain call, and class bodies are strict mode, so this is undefined. Binding fixes it. This is exactly what happens when you write button.addEventListener('click', counter.increment) in a browser or setTimeout(counter.increment, 100). In modern code you’ll often see an arrow function wrapper (() => counter.increment()) or a class field arrow (increment = () => {...}) doing the same job. Both are fine; know that they solve the same problem as bind.
call: borrowing a method from another type
call lets an object use a method it doesn’t own, as long as the method only needs what the object actually has. The old arguments object is array-like but has no reduce:
function sumArgs() {
return Array.prototype.reduce.call(arguments, (a, b) => a + b, 0);
}
console.log(sumArgs(1, 2, 3)); // 6The same trick works for a NodeList in the browser (Array.prototype.map.call(document.querySelectorAll('li'), ...)), though Array.from and rest parameters make it less common than it was. You’ll also see Object.prototype.hasOwnProperty.call(obj, key), which is safe even when obj was created with Object.create(null) or has its own property called hasOwnProperty. Object.hasOwn(obj, key) is the modern replacement.
apply: mostly replaced by spread
Before ES2015, apply was how you passed an array to a function that wanted separate arguments, such as Math.max.apply(null, nums). Today Math.max(...nums) does the same thing and reads better. Both share a limit worth knowing:
const big = Array.from({ length: 1_000_000 }, (_, i) => i);
try { Math.max.apply(null, big); } catch (e) { console.log('apply:', e.constructor.name + ': ' + e.message); }
try { Math.max(...big); } catch (e) { console.log('spread:', e.constructor.name + ': ' + e.message); }
console.log(big.reduce((m, n) => (n > m ? n : m), -Infinity));apply: RangeError: Maximum call stack size exceeded
spread: RangeError: Maximum call stack size exceeded
999999Every element becomes an actual argument, and engines cap how many arguments a call can carry. The exact threshold depends on the engine and stack size, so don’t quote a number in an interview. Say that for very large arrays you loop or reduce instead.
The traps interviewers probe
Removing a listener you bound inline. Each call to bind returns a brand-new function, so c.increment.bind(c) === c.increment.bind(c) is false. Node’s EventEmitter shows the consequence:
bus.on('msg', logger.log.bind(logger));
bus.off('msg', logger.log.bind(logger));
console.log('listeners after off:', bus.listenerCount('msg'));listeners after off: 1The off call was given a different function, so nothing was removed. Keeping the bound function in a variable and passing that same reference to both calls gets the count back to 0. removeEventListener in the browser behaves the same way, and this leak is a common follow-up.
Passing null as thisArg. In strict code (modules, classes), fn.call(null) really sets this to null, and fn.call(5) gives the number 5 as is. In a sloppy CommonJS script, Node substituted globalThis for null and wrapped 5 into a Number object (typeof reported object). That difference explains why old tutorials show this as window in examples that throw in your module code.
Arrow functions. They have no this of their own, so call, apply and bind can pass arguments to them but can’t change this. In an ES module, (() => this).call({ x: 1 }) returned undefined, because the module’s top-level this is undefined.
Performance myths. You may hear that bind is slow. Old engines were slow at it; current V8 isn’t something to design around. The cost that matters is creating a new bound function on every render or every loop iteration, which breaks identity checks like the listener case above or React’s memoisation.
Practice question: write your own bind
A frequent follow-up is “implement bind without using bind”. Give it a go before reading on. Support a fixed this and partially applied arguments; ignore new unless asked.
Answer and reasoning
Function.prototype.myBind = function (thisArg, ...preset) {
const fn = this;
return function (...later) {
return fn.apply(thisArg, [...preset, ...later]);
};
};
function greet(greeting, mark) { return `${greeting}, ${this.name}${mark}`; }
const hi = greet.myBind({ name: 'Ada' }, 'Hi');
console.log(hi('!'));Hi, Ada!Inside myBind, this is the function being bound (because you called greet.myBind(...), implicit binding), so you capture it as fn. The returned wrapper merges the preset and later arguments in order, then uses apply because the arguments are already in an array. Saying why you chose apply over call here is a nice touch: it shows you know the only real difference between them. If the interviewer pushes on new, explain that the native version detects construction (new.target) and ignores the bound this so that new wins; the wrapper above doesn’t, and its name and length differ from a native bound function.