6 a method DESIGNED to be borrowed
GOAL
Write a method whose whole promise is "I read and write nothing except `this`", so any object of the right shape can rent it with fn.call(foreignObj) — then rent it. Today's version cheats: it closes over its home object, which makes borrowing both pointless (the borrower receives nothing) and harmful (the home object gets quietly overwritten by other players' events).
CONCEPT
this-polymorphism: a function keyed purely off `this` is a behavior, not a feature glued to one object. .call(receiver) lends it to any compatible shape for a single invocation — duck typing at the call site, with no inheritance and no copying involved.
HINT
The fix is mechanical: every `minimapPane.` inside the method body turns into `this.`. That one substitution IS the design change.
MIRRORS
Game engines and UI toolkits do this constantly: a minimap defines one onLevelCompleted handler, then hooks it onto the HERO's levelCompleted event — at runtime it runs with `this` = whichever player fired, not the minimap. Same trick as Array.prototype.slice.call(arguments).
Run
node 06-borrowed-method-minimap-style.cjsSource
'use strict';
// ---------------------------------------------------------------------
// PROTO-DOJO · Module 2 / Exercise 6 — a method DESIGNED to be borrowed
//
// GOAL: Write a method whose whole promise is "I read and write
// nothing except `this`", so any object of the right shape can
// rent it with fn.call(foreignObj) — then rent it. Today's
// version cheats: it closes over its home object, which makes
// borrowing both pointless (the borrower receives nothing) and
// harmful (the home object gets quietly overwritten by other
// players' events).
// CONCEPT: this-polymorphism: a function keyed purely off `this` is a
// behavior, not a feature glued to one object. .call(receiver)
// lends it to any compatible shape for a single invocation —
// duck typing at the call site, with no inheritance and no
// copying involved.
// HINT: The fix is mechanical: every `minimapPane.` inside the
// method body turns into `this.`. That one substitution IS the
// design change.
// MIRRORS: Game engines and UI toolkits do this constantly: a minimap
// defines one onLevelCompleted handler, then hooks it onto the
// HERO's levelCompleted event — at runtime it runs with `this`
// = whichever player fired, not the minimap. Same trick as
// Array.prototype.slice.call(arguments).
//
// Run: node 06-borrowed-method-minimap-style.cjs
// ---------------------------------------------------------------------
const assert = require('node:assert');
const minimapPane = {
id: 'minimap',
hits: [{ at: 1000, damage: 3 }, { at: 2000, damage: 5 }, { at: 3000, damage: 2 }],
renderCount: 0,
onLevelCompleted() {
// -- TODO 1 -- hard-wired to the outer `minimapPane` variable:
// borrowing this method computes the damage range for the WRONG
// object and pollutes the minimap with other players' events.
// Rewrite the body to touch NOTHING but `this` — that substitution
// is the entire contract of a borrowable method.
const damages = minimapPane.hits.map((h) => h.damage);
minimapPane.damageMin = Math.min(...damages);
minimapPane.damageMax = Math.max(...damages);
minimapPane.renderCount += 1;
return minimapPane.id + ' damage range ' + minimapPane.damageMin + '..' + minimapPane.damageMax;
},
};
const knightPlayer = {
id: 'knight',
hits: [{ at: 1000, damage: 12 }, { at: 2000, damage: 7 }, { at: 3000, damage: 19 }],
renderCount: 0,
};
// The game screen borrows the minimap's handler for a player it was
// never written on — exactly how the engine hooks it onto the hero's
// levelCompleted:
const knightResult = minimapPane.onLevelCompleted.call(knightPlayer);
// -- checks -----------------------------------------------------------
assert.strictEqual(
knightPlayer.damageMin,
7,
'TODO 1: the borrowed call computed NOTHING for knightPlayer (and quietly updated minimapPane instead) — ' +
'the handler must read hits and write the damage range through `this` only'
);
assert.strictEqual(knightPlayer.damageMax, 19, 'TODO 1: the damage range must land on the BORROWER');
assert.strictEqual(knightResult, 'knight damage range 7..19',
'TODO 1: even the id in the message must come from `this`');
assert.strictEqual(knightPlayer.renderCount, 1, 'TODO 1: the borrower owns the side effects');
assert.strictEqual(minimapPane.damageMin, undefined,
'TODO 1: the borrowed call must NOT leak into minimapPane');
assert.strictEqual(minimapPane.renderCount, 0,
'TODO 1: minimapPane was never the receiver of that call');
// called normally (form 2 — receiver left of the dot), it still serves
// home:
const minimapResult = minimapPane.onLevelCompleted();
assert.strictEqual(minimapResult, 'minimap damage range 2..5',
'called AS a method of minimapPane, this = minimapPane — same function, both jobs');
assert.strictEqual(minimapPane.renderCount, 1);
// -- TODO 2 -----------------------------------------------------------
// Borrow the handler for roguePlayer in ONE expression — no copying the
// function onto roguePlayer, no wrapper objects. Replace null with the call.
const roguePlayer = {
id: 'rogue',
hits: [{ at: 1000, damage: 31 }, { at: 2000, damage: 28 }, { at: 3000, damage: 44 }],
renderCount: 0,
};
const rogueResult = null;
assert.strictEqual(rogueResult, 'rogue damage range 28..44',
'TODO 2: fn.call(receiver) rents the behavior to roguePlayer for exactly one invocation');
assert.strictEqual(roguePlayer.damageMax, 44, 'TODO 2: damage range computed for the rogue');
assert.ok(!Object.hasOwn(roguePlayer, 'onLevelCompleted'),
'TODO 2: borrow, do not install — roguePlayer must stay clean');
console.log('PASS — 06-borrowed-method-minimap-style: touch only this, and any shape can rent you');