JavaScript Basics: Keep the Right Receiver with this, call, and bind
A method can work when called on its object and fail when passed elsewhere as a callback. The difference is often its receiver: the value available as this. This tutorial makes that receiver explicit, then compares bound functions with arrow callbacks.
These independent examples were tested on Linux with Node.js v24.19.0. They use built-in features and assertions only, without packages or network requests. ES modules run in strict mode, which makes a missing receiver easier to notice.
Prepare a scratch directory
You need Bash, a text editor, Node.js 24 on your PATH, and the mktemp, rm, and rmdir utilities. Keep the same terminal open. Save each named source file as UTF-8 in this directory; .mjs selects ES modules. The setup stops if creating or entering the scratch directory fails.
node --version
start_dir="$PWD"
work_dir="$(mktemp -d)" || exit 1
cd -- "$work_dir" || exit 1The tested runtime prints:
v24.19.01. See how the call chooses the receiver
Save this as choose-receiver.mjs:
import assert from 'node:assert/strict';
const shelf = {
name: 'Reference',
label(prefix) { return `${prefix}: ${this.name}`; },
};
const label = shelf.label;
assert.equal(shelf.label('Shelf'), 'Shelf: Reference');
assert.throws(() => label('Shelf'), TypeError);
const borrowed = label.call({ name: 'Fiction' }, 'Shelf');
assert.equal(borrowed, 'Shelf: Fiction');
assert.equal(shelf.name, 'Reference');
console.log(shelf.label('Shelf'));
console.log(borrowed);
console.log('Detached call rejected');Run it:
node choose-receiver.mjsOutput:
Shelf: Reference
Shelf: Fiction
Detached call rejectedIn shelf.label(...), the object before the dot supplies this. Assigning that method to label copies the function reference, not its original receiver. A plain call in this module supplies undefined, so reading this.name throws.
call() invokes the function immediately with its first argument as the receiver; later arguments go to the function. The borrowed call reads the other object’s name without modifying either object. See the ECMAScript call specification and MDN’s this reference.
2. Bind a receiver and a leading argument
Save this as bind-label.mjs:
import assert from 'node:assert/strict';
function label(prefix, number) {
return `${prefix} ${this.name} #${number}`;
}
const shelf = { name: 'History' };
const shelfLabel = label.bind(shelf, 'Shelf');
assert.equal(shelfLabel(2), 'Shelf History #2');
assert.equal(shelfLabel.call({ name: 'Ignored' }, 3), 'Shelf History #3');
console.log(shelfLabel(2));
shelf.name = 'Biography';
assert.equal(shelfLabel(4), 'Shelf Biography #4');
assert.notEqual(shelfLabel, label.bind(shelf, 'Shelf'));
console.log(shelfLabel(4));Run it:
node bind-label.mjsOutput:
Shelf History #2
Shelf Biography #4bind() returns a new function instead of calling the original. Here it stores the receiver and the leading prefix argument. A later call supplies number. Calling this bound function through call() cannot replace the stored receiver.
The receiver remains a reference to the same object, not a snapshot. Renaming the shelf changes future labels. Each bind() call also creates a different function object, so retain one bound callback when an API later needs that exact callback for removal. These examples use ordinary calls, not construction with new. See the bind reference.
3. Capture a receiver with an arrow callback
Save this as capture-arrow-receiver.mjs:
import assert from 'node:assert/strict';
const counter = {
value: 0,
makeIncrementer() {
return () => { this.value += 1; return this.value; };
},
};
const increment = counter.makeIncrementer();
const other = { value: 100 };
assert.equal(increment(), 1);
assert.equal(increment.call(other), 2);
assert.equal(other.value, 100);
console.log(`Original counter: ${counter.value}`);
console.log(`Other counter: ${other.value}`);Run it:
node capture-arrow-receiver.mjsOutput:
Original counter: 2
Other counter: 100The outer method receives counter through its method call. Its arrow callback uses that enclosing this; arrows do not create a receiver from how they are called. Consequently, increment.call(other) still updates counter.
Use a normal method when its receiver should follow the caller. Use a bound function or an arrow created inside that method when a later callback should keep the original object. An arrow written directly as an object-literal property would capture the surrounding scope instead, not automatically capture that object. This distinction matters even when the two declarations look similar.
Clean up
In the same terminal, remove only the named files, return to the original directory, and remove the empty scratch directory:
rm -- choose-receiver.mjs bind-label.mjs capture-arrow-receiver.mjs
cd -- "$start_dir" || exit 1
rmdir -- "$work_dir"
unset start_dir work_dirAcknowledgements
This post was created with assistance from AI.