JAVASCRIPT / SCOPE, CLOSURES, AND THIS
call, apply, and bind for explicit binding
Take manual control of this: invoke a function with call or apply, and produce a permanently bound copy with bind, including pre-filled arguments.
What you will learn
- Use call for a this plus loose args, apply when the args already sit in an array
- Produce a permanently bound copy with bind so a detached method keeps its receiver
- Pre-fill leading arguments with bind to build specialized functions
- Predict thisArg coercion: sloppy mode boxes primitives, strict mode passes them through
Understanding call, apply, and bind for explicit binding
Every function is an object, and it inherits call, apply, and bind from Function.prototype. call and apply both invoke the function immediately and set this to whatever you pass as the first argument; the only difference between them is the shape of the remaining arguments. fn.call(obj, 1, 2) lists them out, fn.apply(obj, [1, 2]) hands over one array-like object that the engine spreads for you. Useful mental model: this is an invisible extra parameter, and these two methods let you fill it in by hand instead of letting the dot in obj.fn() decide.
bind is different in kind, not just in syntax. It does not run the function at all; it returns a brand-new function object whose bound this and bound leading arguments are frozen at the moment bind was called. Because that binding is stored inside the new function rather than read from the call site, nothing later can override it: boundFn.call(other) discards other and boundFn.bind(other) makes another copy that still points at the original target. That permanence is exactly why a bound method survives being handed to setTimeout, addEventListener, or forEach, where the caller has no idea which object the method belongs to.
The thisArg you pass does not always arrive untouched. In sloppy-mode functions it is coerced: primitives are wrapped in objects, so fn.call(7) gives you a Number object, and null or undefined is replaced by globalThis. In strict-mode code, module code, and every class body the value passes through exactly as given, so this can legitimately be undefined or the number 7. Arrow functions ignore the thisArg entirely because they have no this slot of their own, so passing one changes nothing but wastes an argument slot.
function describe(prefix, suffix) {
return prefix + this.name + suffix;
}
const dog = { name: 'Rex' };
const cat = { name: 'Mia' };
console.log(describe.call(dog, '<', '>'));
console.log(describe.apply(cat, ['[', ']']));
const describeDog = describe.bind(dog, '{');
console.log(describeDog('}'));
console.log(describeDog.call(cat, '}'));this is an extra argument supplied by the call site, and call, apply, and bind are how you supply it yourself instead of relying on the dot.
Worked examples
Borrowing methods with call and apply
Shows how call and apply let a function run against an object it was never defined on.
const nums = [7, 3, 12, 5];
console.log(Math.max.apply(null, nums));
console.log(Math.max(...nums));
const arrayLike = { 0: 'a', 1: 'b', length: 2 };
console.log(Array.prototype.join.call(arrayLike, '-'));
console.log(Array.prototype.map.call('abc', c => c.toUpperCase()).join(''));Example explained
Line 1Math.max takes separate numbers, so apply is what turns the array into an argument list.
Line 2Spread syntax does the same job here, which is why apply is now mostly needed when the thisArg also matters.
Line 3join only reads this[0], this[1], and this.length, so a plain object with those keys works fine.
Line 4map.call('abc', ...) treats the string as the receiver and returns a real array, hence the extra join.
bind rescues a detached method
Demonstrates the lost-receiver failure and two ways to fix it when passing a method to forEach.
class Counter {
constructor() { this.count = 0; }
increment() { this.count += 1; }
}
const ticks = [10, 20, 30];
const a = new Counter();
try {
ticks.forEach(a.increment);
} catch (err) {
console.log(err.name);
}
ticks.forEach(a.increment.bind(a));
console.log(a.count);
const b = new Counter();
ticks.forEach(b.increment, b);
console.log(b.count);Example explained
Line 1forEach calls the callback as a plain function, and class bodies are strict, so this is undefined and this.count throws.
Line 2a.increment.bind(a) hands forEach a copy that already knows its receiver, so all three calls land on a.
Line 3forEach's own second parameter is a thisArg, which achieves the same result without allocating a bound function.
Line 4Both counters end at 3 because the array has three elements and each call adds one.
bind as partial application
Shows that bound leading arguments are baked in, and what the returned function looks like.
function tag(name, ...children) {
return `<${name}>${children.join('')}</${name}>`;
}
const li = tag.bind(null, 'li');
console.log(li('one'));
console.log(li('a', 'b'));
console.log(li.name);
console.log(tag.length, li.length);Example explained
Line 1'li' is stored inside the bound function, so later arguments are appended after it, not in front of it.
Line 2null as the thisArg is a signal that tag never reads this; the body only uses its parameters.
Line 3A bound function's name is 'bound ' plus the target's name, which makes stack traces readable.
Line 4length drops from 1 to 0 because one declared parameter has already been supplied.
How the thisArg is coerced
Compares what a sloppy-mode and a strict-mode function receive from the same call.
function sloppy() { return typeof this; }
function strict() { 'use strict'; return typeof this; }
console.log(sloppy.call(7), strict.call(7));
console.log(sloppy.call(null), strict.call(null));
const arrow = () => typeof this;
console.log(arrow.call({ a: 1 }));Example explained
Line 1In sloppy mode the number 7 is boxed into a Number object, so typeof this is 'object'.
Line 2Strict mode skips coercion entirely, so this stays the primitive 7 and null stays null-ish, reported here as undefined.
Line 3sloppy.call(null) gets globalThis substituted for null, which is why it also prints 'object'.
Line 4The arrow ignores the thisArg and keeps the this of the surrounding module or script scope.
Important notes
Re-binding is impossible. bound.call(other) and bound.bind(other) both keep the first this, so store the unbound function if you need per-call flexibility.
new (Fn.bind(obj))() ignores the bound this because the freshly constructed object wins, but the pre-filled arguments still apply.
Common mistakes
Treating bind like call: const total = sum.bind(obj, 1, 2) never runs sum, so total holds a function and arithmetic on it produces NaN. Add the extra () or use call.
Passing apply's arguments as a loose list, as in fn.apply(obj, 1, 2), throws TypeError: CreateListFromArrayLike called on non-object instead of forwarding the numbers.
Calling el.removeEventListener('click', this.onClick.bind(this)) after adding a separately bound handler: each bind creates a new function object, so the original listener stays attached and keeps firing.
Try it yourself
Change, predict, then run
Write greet(greeting, punct) that returns greeting + ' ' + this.name + punct, then call it with call on { name: 'Ada' } and with apply on { name: 'Linus' }. Create const hi = greet.bind({ name: 'Ada' }, 'Hi') and log hi.call({ name: 'Linus' }, '!') to prove the bound receiver still wins.
Open the JavaScript workspaceCheck your understanding
Given function show() { return this.n; }, const bound = show.bind({ n: 1 }), and const other = { n: 2 }, what does console.log(bound.call(other), show.call(other)) print?
- 1 2
- 2 2
- 1 1
- 2 1
Show answer
bound stored its this when bind ran, so call's first argument is discarded and it still returns 1, while the unbound show takes its this from the call site and returns 2. '2 2' is tempting if you assume the most recent call always wins, but a bound function's receiver cannot be overridden once it is baked in.