Lesson 07 / 17
call, apply, and bind
Three operations that determine context at the call site, argument-passing forms, the function-borrowing pattern, partial application, and the identity cost of a permanent bond.
Contents
The previous lesson introduced explicit binding as a rule and showed that the
prototype chain determines where a function will be found while this determines
which data it operates on. Because the two are independent, an object can be run with
a method it has never heard of.
Three operations use this independence. call and apply fix context only for that
one call and run the function immediately; bind produces a new function that
carries the bond permanently. This lesson covers the differences among the three, the
borrowing pattern, and the cost of a permanent bond.
Passing Arguments
The only difference between call and apply is how arguments are given: call
takes them one by one, apply as an array.
const measurementBehavior = { summarize(prefix, separator) { return `${prefix}${separator}${this.sensor}${separator}${this.value}`; }, }; const record = { sensor: "S-01", value: 21.4 }; const args = ["measurement", " | "]; console.log(measurementBehavior.summarize.call(record, "measurement", " | ")); console.log(measurementBehavior.summarize.apply(record, args)); console.log(measurementBehavior.summarize.call(record, ...args)); const values = [21.4, 19.8, 25.1, 18.2]; console.log(Math.max.apply(null, values)); console.log(Math.max(...values)); try { measurementBehavior.summarize.call(null, "measurement", "-"); } catch (error) { console.log(`null context: ${error.constructor.name}`); }
measurement | S-01 | 21.4 measurement | S-01 | 21.4 measurement | S-01 | 21.4 25.1 25.1 null context: TypeError
Once the spread operator entered the language, apply’s argument-unpacking role became
largely unnecessary; the third and fifth lines do the same job with spread. apply
still offers a direct path when the argument list is computed at runtime and held as an
array.
The last line fixes a detail: in strict mode, a null or undefined context is not
converted to the global object, it is passed as is. This is why
Math.max.apply(null, ...) works — Math.max never uses its this value — but a
function that reads this throws an error in the same kind of call.
Function Borrowing
Explicit binding’s real use is function borrowing: running a method not present in an object’s chain, on that object.
const arrayLike = { 0: "S-01", 1: "S-02", 2: "S-03", length: 3 }; console.log(Array.prototype.join.call(arrayLike, ";")); console.log(Array.prototype.map.call(arrayLike, (a) => a.toLowerCase()).join(",")); console.log(Array.from(arrayLike).join(",")); console.log(Array.isArray(arrayLike)); const dict = Object.create(null); dict.sensor = "S-01"; console.log(typeof dict.hasOwnProperty); console.log(Object.prototype.hasOwnProperty.call(dict, "sensor")); console.log(Object.hasOwn(dict, "sensor")); console.log(Object.prototype.toString.call([1, 2])); console.log(Object.prototype.toString.call(null)); console.log(Object.prototype.toString.call(new Date(0))); console.log(Object.prototype.toString.call({ sensor: "S-01" }));
S-01;S-02;S-03 s-01,s-02,s-03 S-01,S-02,S-03 false undefined true true [object Array] [object Null] [object Date] [object Object]
The first three lines show array-like objects: objects carrying numeric keys and a
length property, but with no Array.prototype in their chain. Array methods only
look at these two things, so they can be borrowed. The fourth line confirms the object
is not really an array. Array.from does the same job more readably and is preferred
over borrowing in most cases.
The fifth and sixth lines connect to the first lesson’s null-prototype dictionary. Such
an object has no hasOwnProperty, because it has no chain; the check can only be made
by borrowing from Object.prototype. Object.hasOwn is this borrowing built into the
language, giving the same result with less indirection.
The last four lines take advantage of the fact that Object.prototype.toString
produces a distinctive tag for every value type. This borrowing tells apart cases
typeof cannot — array versus object, null versus object.
Permanent Bond and Partial Application
bind does not run the function; it returns a new function with the context, and
optionally the leading arguments, fixed. Fixing arguments in advance is called
partial application.
function calibrate(offset, multiplier, rawValue) { return (rawValue + offset) * multiplier; } const sensorS01 = calibrate.bind(null, -0.4, 1.02); console.log(calibrate(-0.4, 1.02, 21.4).toFixed(3)); console.log(sensorS01(21.4).toFixed(3)); console.log(sensorS01(19.8).toFixed(3)); console.log(calibrate.length); console.log(sensorS01.length); console.log(sensorS01.name); const record = { sensor: "S-01", value: 21.4, format() { return `${this.sensor}: ${this.value}`; }, }; const boundFormat = record.format.bind(record); const rebound = boundFormat.bind({ sensor: "S-99", value: 0 }); console.log(boundFormat()); console.log(rebound());
21.420 21.420 19.788 3 1 bound calibrate S-01: 21.4 S-01: 21.4
Partial application is the direct way to produce a separate calibration function for
each sensor: the fixed parameters are given once, the varying one on every call. The
function’s length property drops by the number of fixed arguments; its name
property states that it was derived.
The last two lines show a critical rule: a bound function cannot be rebound. The
second bind call produces a new function, but the inner bond does not change, so the
result is unchanged. This same rule was part of the previous lesson’s priority order;
new is its one exception.
Formatting the floating-point outputs with toFixed is deliberate: printing the
expression (21.4 - 0.4) * 1.02 directly would also show digits coming from the
binary-fraction representation. The How Computers Work course’s Floating-Point Numbers
lesson explains this behavior.
Preserving Context in Callbacks
The place the previous lesson’s detachment problem shows up most is a method being passed as a callback. There are three fixes, and all three give the same result.
class MeasurementCollector { constructor(sensor) { this.sensor = sensor; this.values = []; } add(value) { this.values.push(value); } summary() { return `${this.sensor}: ${this.values.join(",")}`; } } const collector = new MeasurementCollector("S-01"); const incoming = [21.4, 19.8, 25.1]; try { incoming.forEach(collector.add); } catch (error) { console.log(`no context: ${error.constructor.name}`); } incoming.forEach(collector.add.bind(collector)); console.log(collector.summary()); const second = new MeasurementCollector("S-02"); incoming.forEach(second.add, second); console.log(second.summary()); const third = new MeasurementCollector("S-03"); incoming.forEach((value) => third.add(value)); console.log(third.summary());
no context: TypeError S-01: 21.4,19.8,25.1 S-02: 21.4,19.8,25.1 S-03: 21.4,19.8,25.1
The first fix produces a permanent bond with bind. The second uses the context
argument some array methods, such as forEach, take as a second parameter — this
option is also found in map, filter, some, and every, but not in every
higher-order function. The third wraps the call in an arrow function and requires no
binding at all; the reason is the next lesson’s subject.
The Identity Cost of a Bond
Every time bind is called, it produces a new function object. This has consequences
everywhere identity comparison is relied on.
const record = { sensor: "S-01", format() { return `record: ${this.sensor}`; }, }; const first = record.format.bind(record); const second = record.format.bind(record); console.log(first() === second()); console.log(first === second); console.log(first === record.format); class Listener { constructor(name) { this.name = name; this.handle = this.handle.bind(this); } handle(event) { return `${this.name} <- ${event}`; } } const a = new Listener("A"); const b = new Listener("B"); console.log(a.handle("measurement")); console.log(Object.hasOwn(a, "handle")); console.log(a.handle === b.handle); console.log(Listener.prototype.handle === Object.getPrototypeOf(a).handle); console.log(a.handle === Listener.prototype.handle);
true false false A <- measurement true false true false
Two bind calls give two functions that produce the same result but are not equal to
each other. Because removing an event listener after registering it requires the same
function reference, the result of bind must be stored; a function rebound each time
cannot be removed.
The second part shows the effect binding done in a class constructor has on the
prototype model. The line this.handle = this.handle.bind(this) creates an own
property that shadows the prototype’s method. The result: every instance carries its
own bound function, sharing across instances is lost, and the number of functions in
memory grows with the instance count. The original definition on the prototype stays
in place but is no longer used.
This is the reverse side of the first lesson’s trade-off: shared behavior is cheap when it stays on the chain, expensive but context-guaranteed once copied to the instance. The Closures lesson will cover this trade-off again, in a more general form.
Summary
callandapplyfix context only for that one call; the only difference is whether arguments are given one by one or as an array.- In strict mode, a
nullorundefinedcontext is not converted to the global object. - Function borrowing runs a method on an object it is not in the chain of; array-like objects and null-prototype dictionaries are the typical cases.
bindproduces a new function, can partially fix arguments, and the bond it produces cannot be replaced by rebinding.- Every
bindcall produces a new identity; for removable listeners, the result must be stored. - Binding done in a constructor shadows the prototype’s method with a per-instance copy and removes sharing.
Next Step
In the previous section, the third fix for the callback problem was an arrow function,
and it required no binding operation at all. The reason is that arrow functions do not
take their this value from the call site — they have no context of their own. The
next lesson examines this behavior, the convenience it provides, and the cases where an
arrow function cannot be used as a method.
To keep your progress and take notes, Log in
My notes
Log in to take notes.