js-dojo

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

Source

'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');

Solution