Learn JS Series (#23) - call, apply, and bind: Controlling this Explicitly
Learn JS Series (#23) - call, apply, and bind: Controlling this Explicitly
What will I learn
- You will learn exactly what
call,apply, andbinddo, and how they differ; - how to borrow a method from one object and run it on another, with no inheritance;
- how
bindcreates a permanently locked function, and where that solves real bugs; - how to partially apply arguments with
bindto build specialized functions; - when arrow functions make these tools unnecessary, and when you still need them.
Requirements
- A working modern computer running macOS, Windows or Ubuntu;
- An installed Node.js (20+) distribution, or just a modern browser console;
- Episodes 1-22 read, especially the five
thisrules from last time.
Difficulty
- Intermediate
Curriculum (of the Learn JS Series):
- Learn JS Series (#1) - What Is JavaScript, Why It Runs Everywhere, and How to Run It
- Learn JS Series (#2) - Variables and Bindings
- Learn JS Series (#3) - The Primitive Types: number, string, boolean, null, undefined, symbol, bigint
- Learn JS Series (#4) - Operators and Expressions: Arithmetic, Comparison, Logical, and Short-Circuiting
- Learn JS Series (#5) - Strings: Template Literals, Unicode, and the Methods You Actually Use
- Learn JS Series (#6) - Numbers: IEEE 754, Why 0.1 + 0.2 Is Not 0.3, and How to Cope
- Learn JS Series (#7) - Control Flow: if/else, switch, and the Ternary Expression
- Learn JS Series (#8) - Loops: for, while, for...of, for...in, and When to Use Which
- Learn JS Series (#9) - Functions: Declarations, Parameters, Return Values, and Hoisting
- Learn JS Series (#10) - Scope and the Temporal Dead Zone: How JavaScript Finds Your Variables
- Learn JS Series (#11) - Arrays: The Workhorse Data Structure and Its Core Methods
- Learn JS Series (#12) - Objects: Key-Value Data, Dot vs Bracket Access, and Nesting
- Learn JS Series (#13) - Truthiness, Equality, and Coercion: == vs === Done Properly
- Learn JS Series (#14) - Mini Project: A Command-Line Tip Calculator
- Learn JS Series (#15) - First-Class Functions: Passing, Returning, and Storing Functions
- Learn JS Series (#16) - Arrow Functions vs function: Syntax, this, and When Each Wins
- Learn JS Series (#17) - Closures: The Single Most Important Idea in JavaScript
- Learn JS Series (#18) - Higher-Order Functions: Functions That Take or Return Functions
- Learn JS Series (#19) - Callbacks and the Callback Pattern (Before We Reach Promises)
- Learn JS Series (#20) - Default, Rest, and Spread: Flexible Function Signatures
- Learn JS Series (#21) - Destructuring Parameters: Named Arguments the JS Way
- Learn JS Series (#22) - The this Keyword: Five Rules That Explain Every Case
- Learn JS Series (#23) - call, apply, and bind: Controlling this Explicitly (this post)
Learn JS Series (#23) - call, apply, and bind: Controlling this Explicitly
Solutions to Episode 22 Exercises
Exercise 1 - method call versus detached call:
const dog = {
name: "Rex",
speak() { return `${this.name} says woof`; },
};
console.log(dog.speak()); // "Rex says woof" - rule 1, 'this' is dog
const detached = dog.speak;
// console.log(detached()); // ERROR (runtime): rule 2, 'this' is undefined
The insight: dog.speak() is a method call (rule 1), so this is the object before the dot; detached() is a plain call (rule 2) with this === undefined, and this.name then throws.
Exercise 2 - fixing this with call and bind:
function introduce() { return `I am ${this.name}`; }
const person = { name: "scipio" };
console.log(introduce.call(person)); // "I am scipio" - runs now
const bound = introduce.bind(person);
console.log(bound()); // "I am scipio" - locked function
The insight: call invokes immediately with a chosen this; bind returns a new function permanently tied to that this that you can store and call later.
Exercise 3 - regular callback versus arrow:
const box = {
size: 5,
reportLater() {
// setTimeout(function () { console.log(this.size); }, 10); // rule 2: undefined
setTimeout(() => { console.log(this.size); }, 10); // rule 5: keeps box
},
};
// box.reportLater(); // arrow prints 5; the regular function would fail
The insight: the regular callback is a plain call (rule 2, this undefined); the arrow uses the enclosing this (rule 5, the box), which is why it just works.
Right, with the five rules fresh, let's give rule 3 the full treatment it deserves. Last episode I called call, apply, and bind "a taste" and promised the real meal this time. Here it is. These three are the tools you reach for when you do NOT want to leave this to the mercy of the call site -- when you want to set it by hand, precisely, and know exactly what it will be.
Every function has call, apply, and bind
Start with a fact that explains why these three even exist: in JavaScript, functions are objects (a Phase 3 detail we will unpack properly later, but take it on trust for now). Because a function is an object, it can carry methods of its own, and every function you ever write inherits three of them straight from Function.prototype: call, apply, and bind. You did not add them, they are just there, on every function, always.
All three answer the same question -- "what should this be when this function runs?" -- and they let YOU answer it instead of the call site. They differ along just two axes: whether they run the function right now or hand you a new function for later, and how they take the function's normal arguments. Get those two axes straight and there is genuinely nothing else to learn here. Let's take them one at a time.
call: run now, arguments listed
call invokes the function immediately. Its first argument becomes this, and every argument after that is passed through to the function as its normal parameters, listed out one by one:
function greet(greeting, punctuation) {
return `${greeting}, ${this.name}${punctuation}`;
}
const user = { name: "scipio" };
console.log(greet.call(user, "Hi", "!")); // "Hi, scipio!"
Read that call out loud as a sentence and it stops being mysterious: "call greet, set its this to user, and pass "Hi" and "!" as the ordinary arguments." That is the whole of call. It is the most direct way in the language to run a function with a this that you pick, right now, this instant.
One thing worth pinning down: the FIRST argument is special (it is the this value), and everything else shifts over to become the real parameters. So greet.call(user, "Hi", "!") maps user to this, "Hi" to greeting, and "!" to punctuation. Miscounting that offset is the single most common call mistake, so keep the mental picture that the first slot is "stolen" by this.
apply: run now, arguments as an array
apply does exactly what call does, with one solitary difference: it takes the function's arguments as a single array instead of listed out individually. That is the only distinction between them. call spreads, apply packs:
console.log(greet.apply(user, ["Hey", "."])); // "Hey, scipio."
A memory aid that has stuck with me for years: apply takes an array, call takes a comma-separated list. Same first letter, same shape. Once that clicks you never mix them up again.
So when is packing the arguments into an array actually useful? When you already HAVE them in an array and do not want to unpack them by hand. The classic example is calling a function that expects separate numbers, on a list of numbers you already hold:
const nums = [5, 12, 8, 130, 44];
console.log(Math.max.apply(null, nums)); // 130 - array spread as separate args
console.log(Math.max(...nums)); // 130 - the modern spread equivalent
Math.max wants Math.max(5, 12, 8, ...), not an array, and before the spread operator existed, apply was THE way to feed it an array. Notice we passed null as the first argument, because Math.max does not use this at all, so any placeholder will do. In modern code the spread operator ... has made apply largely redundant (you can write greet.call(user, ...args) and get the same effect), but you will still meet apply constantly in existing code, so you must be able to read it fluently.
Method borrowing: the real payoff
Here is where call and apply genuinely earn their keep, not as trivia but as a technique: method borrowing. If an object owns a useful method, you can run that method with a completely different object as its this, without any inheritance, without any shared prototype, without copying anything. You just point the method's this wherever you like:
const person = {
fullName() { return `${this.first} ${this.last}`; },
};
const other = { first: "scipio", last: "the great" };
console.log(person.fullName.call(other)); // "scipio the great"
other has no fullName method of its own. It does not inherit one. But we grabbed person's method and ran it with this aimed at other, and because fullName only ever touches this.first and this.last, it works perfectly. That is method borrowing in one line: a method is just a function that reads this, so give it whichever this you want.
This was historically vital for a very practical reason. Old-school JavaScript had "array-like" objects -- things with a length and numeric indices but WITHOUT the real array methods. The arguments object inside a function is the classic case. To use real array methods on them, you borrowed from Array.prototype:
function collectArgs() {
// borrow Array's slice to turn array-like 'arguments' into a real array
return Array.prototype.slice.call(arguments);
}
console.log(collectArgs(1, 2, 3)); // [1, 2, 3]
arguments is not a real array, so it has no .slice(). But slice only cares that its this has a length and indexed elements, so borrowing it via call works a treat. Modern code writes [...arguments] or a rest parameter (...args) in stead, but you will run into Array.prototype.slice.call(arguments) in older codebases forever, and now you know exactly what it is doing and why. Understanding this one idiom demystifies quit a lot of pre-2015 JavaScript.
bind: create a permanently locked function
Now for the different one. call and apply both run the function immediately. bind does NOT. Instead, bind returns a brand new function whose this is permanently locked to whatever you passed. It does not invoke anything -- it manufactures a new function for you to call whenever you please, and no matter how you later call it, its this cannot change:
const counter = {
count: 0,
increment() { this.count += 1; return this.count; },
};
const inc = counter.increment.bind(counter); // locked to counter forever
console.log(inc()); // 1 - works even though called as a plain function
console.log(inc()); // 2
That inc is a plain, bare function now -- there is no counter. in front of it -- and yet it still increments counter correctly, because the this is baked in. This is the definitive cure for the "lost this" bug we diagnosed last episode. Remember the trap: the moment you pass a method somewhere as a callback, it gets detached, rule 2 kicks in, and this becomes undefined. A bound method carries its this with it, so it survives the trip:
// safe to pass around: it will always have the right 'this'
setTimeout(counter.increment.bind(counter), 100); // increments correctly later
And the lock is genuinely permanent -- this is important, so let me show it. Once a function is bound, you cannot re-point its this even with call. The first bind wins, permanently:
function whoAmI() { return this.label; }
const a = { label: "A" };
const b = { label: "B" };
const boundToA = whoAmI.bind(a);
console.log(boundToA()); // "A"
console.log(boundToA.call(b)); // "A" - bind wins, call cannot override it
That permanence is exactly the property you want for event handlers and stored callbacks. A hugely common real-world pattern is to bind a method once in a constructor, so that every copy of that handler you hand out is already safe to detach:
class Button {
constructor(label) {
this.label = label;
this.handleClick = this.handleClick.bind(this); // lock 'this' once, up front
}
handleClick() {
return `clicked ${this.label}`;
}
}
const save = new Button("Save");
const handler = save.handleClick; // detached, but already bound in the constructor
console.log(handler()); // "clicked Save" - survives detachment
Before arrow functions existed, bind was THE canonical way to keep this correct in callbacks, and you will see that constructor-binding pattern in mountains of older class-based code (React components from the mid-2010s are packed with it). Arrows now handle many of those cases more cleanly, but bind is still the right tool when you need a reusable, detachable function that is permanently welded to a specific object.
Partial application with bind
bind has a bonus feature that is easy to miss and genuinely useful. Besides fixing this, any EXTRA arguments you pass to bind get pre-filled into the returned function, locked in ahead of time. This lets you manufacture a specialized function with some of its arguments already baked in:
function multiply(a, b) {
return a * b;
}
const double = multiply.bind(null, 2); // 'this' unused, so null; a is fixed to 2
console.log(double(5)); // 10 - calls multiply(2, 5)
console.log(double(10)); // 20
const triple = multiply.bind(null, 3);
console.log(triple(5)); // 15
We passed null for this (because multiply never touches this, so it does not matter), and pre-filled a as 2. The result is a double function that only needs b. We just built a new, more specific function out of a general one by freezing one of its arguments. That technique has a name -- partial application -- and it previews the currying ideas coming a few episodes down the line. bind hands you a taste of it for free, no library required.
You can pre-fill more than one argument, too. Fix both and you have effectively created a constant-producing function:
function label(prefix, level, msg) {
return `${prefix}[${level}] ${msg}`;
}
const warn = label.bind(null, "APP", "WARN"); // pre-fill prefix and level
console.log(warn("disk almost full")); // "APP[WARN] disk almost full"
console.log(warn("cache miss")); // "APP[WARN] cache miss"
The arguments fill left to right, so bind's pre-filled values always take the FIRST parameter slots, and whatever you pass at call time fills the rest. That ordering (fixed arguments first, variable arguments last) is worth remembering, because it is exactly why library functions meant for partial application put their "configuration" parameters first and their "data" parameter last.
When arrows make these unnecessary
Since arrow functions lock this lexically (rule 5 from last episode), they quietly replace bind in a whole category of situations. If you are writing a callback INSIDE a method and all you want is the surrounding this, an arrow is cleaner than binding anything:
const service = {
name: "api",
logLater() {
// no bind needed - the arrow keeps logLater's 'this'
setTimeout(() => console.log(`${this.name} ready`), 10);
},
};
// service.logLater(); // "api ready"
So when do you still reach for call, apply, or bind? Three cases, and they are worth memorizing as a checklist. First, when you need to run a function with a specific object's this right now -- that is method borrowing, and only call/apply do it. Second, when you need a REUSABLE, DETACHABLE function permanently locked to an object -- event handlers, callbacks you store for later, anything you hand off that must not lose its this -- that is bind. Third, when you want partial application -- again bind. Arrows cover exactly one thing: "keep whatever this I already have here." These three cover the other thing: "set this to something specific." They are not competitors, they solve different problems.
How other languages handle this
A good few of you came to this series from the Learn Python Series, with some Rust and Go folks mixed in, so a look sideways helps put JavaScript's design in perspective. The short version: other languages tend to give you partial application and stable receivers through closures and bound methods, without needing a special bind.
Python has a bound method for free -- pulling a method off an instance keeps the instance attached, so the JavaScript detachment bug simply does not exist. And for partial application it ships functools.partial, which is bind's argument-fixing feature as a standalone tool:
from functools import partial
def multiply(a, b):
return a * b
double = partial(multiply, 2) # pre-fill the first argument, like bind
print(double(5)) # 10
print(double(10)) # 20
class Counter:
def __init__(self):
self.count = 0
def increment(self):
self.count += 1
return self.count
c = Counter()
inc = c.increment # a bound method - remembers c, no bind() needed
print(inc()) # 1
Notice Python splits the two jobs bind does into two mechanisms: the bound method handles self, and partial handles pre-filled arguments. JavaScript's bind bundles both into one call.
Rust does not have this at all in the wandering JavaScript sense -- the receiver is an explicit self parameter checked by the compiler. For the "specialize a function by fixing an argument" job, you reach for a closure, which captures the fixed value:
fn multiply(a: i32, b: i32) -> i32 {
a * b
}
fn main() {
let double = |b| multiply(2, b); // a closure captures the fixed argument
println!("{}", double(5)); // 10
println!("{}", double(10)); // 20
}
The closure |b| multiply(2, b) IS partial application -- it is just spelled with Rust's closure syntax rather than a bind method. Same idea, different clothes.
Go, minimalist as ever, gives you method values (a method pulled off a value keeps its receiver) and plain closures for fixing arguments:
package main
import "fmt"
func multiply(a, b int) int {
return a * b
}
func main() {
double := func(b int) int { return multiply(2, b) } // closure fixes a = 2
fmt.Println(double(5)) // 10
fmt.Println(double(10)) // 20
}
Three languages, one recurring pattern: partial application is really just a closure that remembers a fixed value, and a stable receiver is really just a method that stays attached to its object. JavaScript's call/apply/bind are the explicit, verbose versions of ideas that other languages fold into closures and bound methods. Having said that, once you see bind as "closure that also fixes this", the whole family clicks into place and stops feeling like special magic. ;-)
Try it yourself
Three exercises, increasing in difficulty. Type them out and predict the output before you run them -- the gap between your guess and the actual result is where the real learning happens. Full solutions open the next episode.
- Write a function
sayCity()that returnsthis.city, and two objects{ city: "amsterdam" }and{ city: "berlin" }. Usecallto runsayCitywith each. Then rewrite one of the calls usingapplyand note the only difference between the two. - Write a
multiply(a, b, c)that returnsa * b * c, and usebindto create atimes12function that fixesaandbto3and4, sotimes12(2)returns24. Explain in one line what you passed asbind's FIRST argument and why. - Given
const logger = { prefix: "[LOG]", write(msg) { return this.prefix + " " + msg; } }, create a detachablewritefunction bound tologgerand pass it into asetTimeout. Show it still uses the right prefix, and explain why an unbound version would fail using rule 2.
So what did we actually cover?
- Every function inherits
call,apply, andbindfromFunction.prototype; all three let YOU decide whatthiswill be, in stead of the call site. callruns the function now with a chosenthisand comma-separated arguments;applyis identical but takes the arguments as an array (apply = array).- Method borrowing uses
call/applyto run one object's method with another object asthis-- no inheritance needed, because a method is just a function that readsthis. bindreturns a NEW function permanently locked to athis(and the lock cannot be overridden, even bycall), which is the definitive fix for the "lost this" bug when passing methods as callbacks.- Extra arguments to
bindare pre-filled left to right (partial application), letting you build specialized functions likedouble = multiply.bind(null, 2). - Arrow functions replace
bindwhen you just want the surroundingthis; usecall/apply/bindwhen you must set a SPECIFICthisor partially apply arguments.
Next episode we change gears completely and look at recursion: functions that call themselves, the base case that stops them from running forever, how the call stack actually grows with each call, and the dreaded stack overflow that happens when it grows too far.