1 the capture rule
GOAL
predict what a stored callback reports by asking one question: which variable slot does this closure link to, and what is in that slot right NOW?
CONCEPT
a closure captures the variable, not the value. A callback keeps a live link to the variable's storage slot and reads that slot when it is called, not when it was created.
HINT
count the slots. `var` makes ONE slot for the whole function; `let` in a loop head makes a fresh slot per iteration; and a parameter is a slot like any other.
MIRRORS
the skill tree panel on the HUD. Every branch skill builds its own expand callback while the panel redraws, closing over that redraw's skill id. A fresh binding per redraw is the let-loop rule at panel scale; one shared binding is the var-loop bug, where each node expands whichever skill the redraw happened to finish on.
Run
node 01-the-capture-rule.cjsSource
// DOJO · Module 9 / Exercise 1 — the capture rule
// GOAL: predict what a stored callback reports by asking one question:
// which variable slot does this closure link to, and what is in
// that slot right NOW?
// CONCEPT: a closure captures the variable, not the value. A callback
// keeps a live link to the variable's storage slot and reads that
// slot when it is called, not when it was created.
// HINT: count the slots. `var` makes ONE slot for the whole function;
// `let` in a loop head makes a fresh slot per iteration; and a
// parameter is a slot like any other.
// MIRRORS: the skill tree panel on the HUD. Every branch skill builds
// its own expand callback while the panel redraws, closing over
// that redraw's skill id. A fresh binding per redraw is the
// let-loop rule at panel scale; one shared binding is the
// var-loop bug, where each node expands whichever skill the
// redraw happened to finish on.
// Run: node 01-the-capture-rule.cjs
'use strict';
const assert = require('node:assert');
// --- TODO 1 --------------------------------------------------------
// Three HUD callbacks are registered inside a `var` loop, one per skill
// tree node. `var i` creates ONE slot for the whole file, and the loop
// keeps overwriting it. Each callback links to that same slot, and the
// calls below happen after the loop has already finished.
// In place of null, write the three numbers map() hands back.
const varNodeReaders = [];
for (var i = 0; i < 3; i++) {
varNodeReaders.push(() => i);
}
const answerVar = null; // your answer, e.g. [9, 9, 9]
assert.deepStrictEqual(
varNodeReaders.map((read) => read()),
answerVar,
'TODO 1 — what does each var-loop callback report?',
);
// --- TODO 2 --------------------------------------------------------
// The same loop over the same three nodes, but with `let`. The language
// rule: `let` in a loop head creates a fresh slot for every iteration,
// holding that iteration's value. Each callback links to its own slot.
// In place of null, write the three numbers map() hands back.
const letNodeReaders = [];
for (let j = 0; j < 3; j++) {
letNodeReaders.push(() => j);
}
const answerLet = null; // your answer
assert.deepStrictEqual(
letNodeReaders.map((read) => read()),
answerLet,
'TODO 2 — what does each let-loop callback report?',
);
// --- TODO 3 --------------------------------------------------------
// A parameter is a variable slot like any other. makePointsReader makes
// a closure over `points`, THEN reassigns `points`, then hands the
// closure back. The closure reads the slot at call time, not a snapshot
// taken when it was created.
// In place of null, write the number readPoints() gives you.
function makePointsReader(points) {
const readPoints = () => points;
points = points * 10;
return readPoints;
}
const readPoints = makePointsReader(4);
const answerParam = null; // your answer
assert.strictEqual(
readPoints(),
answerParam,
'TODO 3 — what does readPoints() return?',
);
// --- TODO 4 --------------------------------------------------------
// Now fix one. makeExpanders should hand back three callbacks reporting
// 'skill-0', 'skill-1', 'skill-2' — one expander per skill tree node.
// Today all three link to a single `k` slot and every one of them
// reports 'skill-3'.
// Fix the loop so each expander captures its own index. One keyword is
// enough.
function makeExpanders() {
const expanders = [];
for (var k = 0; k < 3; k++) {
expanders.push(() => `skill-${k}`);
}
return expanders;
}
assert.deepStrictEqual(
makeExpanders().map((expand) => expand()),
['skill-0', 'skill-1', 'skill-2'],
'TODO 4 — each expander must capture its own index',
);
console.log('PASS');