2 the extracted method loses its this
GOAL
Trigger and then cure the most frequent `this` bug in UI and game code: handing `obj.method` over as a callback. The `obj.` half never gets packed into the function reference — it is gone the instant the reference is stored, and the eventual plain call arrives receiverless.
CONCEPT
`screen.handleResize` is only a lookup; what comes back is the naked function. The emitter later runs `handler()` — form 1, this === undefined. Cures: bind where you subscribe, wrap the call in an arrow, or (class component style) bind once in the constructor / use a class field.
HINT
Both broken spots need the receiver to TRAVEL WITH the function: fn.bind(receiver) manufactures a fresh function with this welded on.
MIRRORS
Component frameworks: `<Canvas onResize={this.handleResize} />` dying with "cannot read gameId of undefined"; the same tale with a third-party engine's `engine.on('resize', this.onResize)` in a wrapper.
Run
node 02-extracted-method-loses-this.cjsSource
'use strict';
// ---------------------------------------------------------------------
// PROTO-DOJO · Module 2 / Exercise 2 — the extracted method loses its
// `this`
//
// GOAL: Trigger and then cure the most frequent `this` bug in UI and
// game code: handing `obj.method` over as a callback. The
// `obj.` half never gets packed into the function reference —
// it is gone the instant the reference is stored, and the
// eventual plain call arrives receiverless.
// CONCEPT: `screen.handleResize` is only a lookup; what comes back is
// the naked function. The emitter later runs `handler()` —
// form 1, this === undefined. Cures: bind where you subscribe,
// wrap the call in an arrow, or (class component style) bind
// once in the constructor / use a class field.
// HINT: Both broken spots need the receiver to TRAVEL WITH the
// function: fn.bind(receiver) manufactures a fresh function
// with this welded on.
// MIRRORS: Component frameworks: `<Canvas onResize={this.handleResize}
// />` dying with "cannot read gameId of undefined"; the same
// tale with a third-party engine's `engine.on('resize',
// this.onResize)` in a wrapper.
//
// Run: node 02-extracted-method-loses-this.cjs
// ---------------------------------------------------------------------
const assert = require('node:assert');
class GameScreen {
constructor(gameId) {
this.gameId = gameId;
this.resizeCount = 0;
}
handleResize() {
this.resizeCount += 1;
return 'resized:' + this.gameId;
}
}
// A viewport-resize emitter, the kind every engine ships: it keeps BARE
// function references and later fires them as PLAIN calls — no
// receiver,
// no dot, no this.
const resizeHandlers = [];
function onResize(handler) {
resizeHandlers.push(handler);
}
function fireResize() {
return resizeHandlers.map((handler) => handler()); // plain call!
}
const screen = new GameScreen('match-1');
// -- TODO 1 -----------------------------------------------------------
// The classic component-callback bug: this gives the emitter a NAKED function —
// the `screen.` prefix is dropped the moment the reference is stored.
// Fix THIS LINE so the handler keeps its screen (bind it, or wrap in an arrow).
onResize(screen.handleResize);
let results;
let boom;
try {
results = fireResize();
} catch (err) {
boom = err;
}
assert.strictEqual(
boom,
undefined,
'TODO 1: firing resize blew up with "' + (boom && boom.message) + '" — the extracted method ran as a ' +
'PLAIN call, so this === undefined. The call site picks `this`; the subscription must carry the receiver along.'
);
assert.deepStrictEqual(results, ['resized:match-1'], 'the handler must reach the real screen');
assert.strictEqual(screen.resizeCount, 1, 'the real screen state must have been updated');
// -- TODO 2 -----------------------------------------------------------
// Same disease, no emitter: build a standalone, pass-anywhere version of
// handleResize that is PERMANENTLY welded to `screen`, starting from the
// bare `detached` reference below.
const detached = screen.handleResize; // bare — `detached()` would explode
const weldedToScreen = detached;
assert.strictEqual(typeof weldedToScreen, 'function',
'TODO 2: Function.prototype.bind returns a NEW function — it does not call anything');
assert.strictEqual(weldedToScreen(), 'resized:match-1',
'TODO 2: the bound this must survive a plain call');
assert.strictEqual(screen.resizeCount, 2, 'the bound call must hit the same screen state');
console.log('PASS — 02-extracted-method-loses-this: receivers do not travel with function references');