Skip to content
academia.sh

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

  • call and apply fix 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 null or undefined context 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.
  • bind produces a new function, can partially fix arguments, and the bond it produces cannot be replaced by rebinding.
  • Every bind call 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.

Start typing to search.

↑↓ Esc navigate · open · close