js-dojo

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

Source

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

Solution