In a world with , chapter 3.2.

THE PATH · SESSION 12 OF 66 · UNIT 3 · ACT I

Spell Crafting

Functions, Scope, Closures, and Array Methods

45 min read5 diagrams4 side quests5 runnable3,799 words6 code blocks

At a glance

PACE
Session 12 of 66 · suggested week 4. Continue when you are ready.
UNIT
Unit 3 · My Cheat Code Turned Out to Be JavaScript
ACT
Act I · Foundation
BEFORE
3.1 Mana Basics
AFTER
3.3 The Proving Grounds
ARTIFACT
spellbook.js: ten spells, a closure-based mana pool, and the first two query functions built on filter
READ
45 min · 5 diagrams · 6 code blocks

The hook#

Picture a wizard who has to recite the whole incantation from the first syllable every single time she wants a fireball.

That is you, right now. Last session you learned to hold values and choose between roads, and the NPC you wrote works. But look at the file. The reputation ladder is written out in full at the one place the receptionist uses it, and nothing else can use it. If you wanted a second NPC you would copy the ladder, and a bug in it would then live in two places. The wizard who recites everything from scratch does not get much done, and neither does the programmer who does.

A function is a named spell. You write the incantation once, you give it a name, and from then on you cast it with a single word and a pair of parentheses. That alone would be worth the session. But we are going further, because functions bring two companions that most beginners meet without being introduced.

The first is scope, the set of rules about where a variable lives and dies. The second is the closure, a function that remembers the place it was born and carries that place around with it after the place itself is gone. Closures sound exotic. They are not. You will write six of them today without trying.

Last session you learned to speak. Today you learn to write the spell down, so you never have to say the whole thing twice.

And then the combo attacks. JavaScript arrays come with a family of methods that take a function as their argument and apply it to every element: map, filter, reduce. Chained together, they turn a list of orcs into a list of the dangerous ones, then into a list of their names, then into a single number, in four readable lines and without a single loop counter. By the end of this session that chain will be the most natural thing in the world, and you will have a mana pool that refuses to be overdrawn, built from a closure.

The idea#

Four ideas again, and this time they stack: each one is built from the one before it.

A function is a named spell, and JavaScript spells it three ways. The spell has a name, a list of parameters (the names it expects), a body, and a return value (what it hands back). When you cast it, the values you pass in are the arguments.

Three rows, Declaration, Expression and Arrow, each showing the same calculateDamage function written that way; ticks and crosses beside each note hoisted or not hoisted, own this or no this of its own, and implicit return for the arrow; a strip below contrasts calculateDamage(25, 10) giving 15 with the bare name giving a Function
Fig 1 · One spell, three spellings. Declarations are hoisted; expressions and arrows are not; arrows have no this of their own. Parentheses cast the spell, the bare name is the scroll.

A function declaration starts with the word function and is hoisted: the whole thing is registered in the first pass you met last session, so you can call it on a line above the one where it is written. A function expression stores a function in a variable, usually a const; it is not hoisted, because the variable is in the temporal dead zone until its line. That zone is the one 3.1's two passes left behind: pass one registers the name, pass two reaches the line that fills it, and reading the name anywhere between the two throws a ReferenceError. An arrow function is the short modern spelling: (a, b) => a + b. If the body is a single expression, it is returned without the word return. Arrows also have no this of their own, which matters a great deal in Session 3.4 and not at all today.

Which to use? Declarations for the named, top-level spells in a file. Arrows for everything you pass to something else, which has a name: a callback is a function you hand to another function so that it can call it for you, and the little transformers inside map and filter are the callbacks you will write most. Expressions when you need a function in a variable and want the long spelling for some reason, which is rare. Default parameters (name = "Adventurer") fill in missing arguments. And the distinction you will trip on this week: calculateDamage(25, 10) with parentheses casts the spell and gives you 15; calculateDamage without them refers to the spell itself, the scroll rather than the fire. You hand scrolls to map. You cast fire everywhere else.

Scope is a set of nested rings, and lookups walk outward. A variable declared outside every function and block is global; it is visible everywhere, which is why you want as few of them as possible. A variable declared inside a function is function-scoped; it exists only while that function runs. A let or const inside braces is block-scoped; it exists only inside those braces.

Three nested rounded rings labelled global scope with worldName, function scope enterDungeon with dungeonLevel, and block scope with boss and loot; beside them five lookups, three green ones reading outward succeed and two red ones reading boss after the block or dungeonLevel outside the function throw a ReferenceError
Fig 2 · The scope chain. Lookups walk outward through the rings and never inward; var ignores the innermost ring entirely.

When code mentions a name, JavaScript looks in the current ring, then the ring around it, then the ring around that, out to the global ring. That outward walk is the scope chain. The walk only goes one way. Code inside the if block can read dungeonLevel from the function around it and worldName from the world around that; code after the block cannot read boss, because the block's ring has closed. The two ReferenceErrors in the figure are the ones you will see most this week, and both have the same cause: trying to look into a ring from outside it.

A closure is a function carrying a backpack. Here is the sentence to memorise: a function remembers the scope it was created in, even after that scope has finished. That memory is the closure.

A dashed, faded createCounter function marked finished and gone; to its right two returned functions each carrying a small backpack icon with count inside, hitCounter on 3 after three calls returning 1, 2, 3, and comboCounter on 1 after one call
Fig 3 · The backpack. The outer call is over; the inner function left carrying count. Every call to the factory packs a new backpack, and nothing outside can open one.

createCounter runs, declares count, returns an inner function, and finishes. Its ring should be gone. But the inner function mentioned count, so when it left, it took count with it in a backpack. Every call to hitCounter() opens the backpack, adds one, and puts it back. Nothing outside can reach into that backpack; count is as private as a variable can be in JavaScript. And a second call to createCounter() packs a brand-new backpack with its own count, which is why comboCounter starts at one while hitCounter, on three in the figure, carries on to four when the run block calls it once more.

This is not an advanced feature you will use someday. It happens every time an inner function mentions an outer variable, which is to say constantly. Counters, private state, factories that produce configured functions (createAttack(50) gives you a fireball that always knows its base damage), the mana pool in today's lab. Closures are also the mechanism under event handlers and asynchronous code, so the backpack picture will keep paying you back for the rest of the course.

Array methods are combo attacks: filter fewer, map the same, reduce to one. An array is an ordered list, counted from zero. Some of its methods mutate it in place (push, pop, shift, unshift, splice, sort); others leave it alone: slice and concat hand back a new array, includes and indexOf an answer. Then there is the family that takes a function and applies it per element.

Four stages joined by green arrows: heroes, five object cards; filter power greater than 8000, the same five with two struck through; map to power, three numbers 9001, 8200 and 8500; reduce with a sum, 0 plus the three numbers equals 25701
Fig 4 · The combo chain. filter keeps fewer, map keeps the count but changes the shape, reduce folds to one value; the original array is untouched the whole way.

forEach runs a function on each element and returns nothing; it is for side effects like printing. map runs a function on each element and returns a new array of the results, same length, new shape. filter runs a test on each element and returns a new array of only the ones that passed, fewer or equal, same shape. reduce runs a function that folds each element into an accumulator and returns the single final value: a sum, a count, an object of counts, a maximum. Because each of filter, map and reduce returns a new value, you can hand that value straight to the next one, and that hand-off is the chain in the figure. The original array is never touched.

There is one more toolset that appears in every modern file you will read, the three dots that unpack an object or pack up leftover arguments, and it is deliberately not on this page. Four ideas is the right load for one session. The dots, the rules for copying an array without changing the original, and the counting pattern built on reduce get a page of their own in Session 3.3, where you also prove, on three small problems with no scaffolding, that today's four ideas are in your hands. One of them does appear once below, in the last Run block, and it is introduced the moment it shows up.

The demonstration#

Five files, in the order you would write them at the desk. Press Run on each and read the output against the code. Two of these blocks wrap a line in try { ... } catch (e) { ... }. That is a way of letting the file keep going after an error, so the block can print the error's name, e.constructor.name, instead of stopping on the spot. Session 4.2 teaches it properly; today you only need to read past it.

1. One spell, three spellings#

Declarations, expressions and arrows, default parameters, call versus reference, and the first sighting of the missing return.

JSthree-ways.js
'use strict';// 1. Declaration: hoisted, so this call works a line above the definition.console.log(calculateDamage(25, 10));function calculateDamage(attack, defense) {  return Math.max(0, attack - defense);} // 2. Expression: a function stored in a variable. Not hoisted.const calculateHeal = function (baseHeal, level) {  return baseHeal + level * 2;};console.log(calculateHeal(20, 5)); // 3. Arrow: short, and the one expression is returned without the word.const calculateCrit = (damage, multiplier = 2) => damage * multiplier;const double = (x) => x * 2;const rollDice = () => Math.floor(Math.random() * 6) + 1;console.log(calculateCrit(40), calculateCrit(40, 3), double(21));const roll = rollDice();console.log('rolled a number between 1 and 6:', roll >= 1 && roll <= 6); // Parameters are the names in the definition; arguments are the values at the call.function greetHero(name = 'Adventurer', title = 'Novice') {  return `Hail, ${name} the ${title}!`;}console.log(greetHero());console.log(greetHero('Guts', 'Berserker')); // Calling versus referencing: the parentheses are the spell.console.log(typeof calculateDamage, '|', calculateDamage.name, '|', calculateDamage(5, 1)); // Forgetting return: the most common bug of the week.function oops(a, b) {  a + b; // computed, then dropped on the floor}console.log('oops(2, 3) ->', oops(2, 3)); // An expression is not hoisted: it sits in the temporal dead zone until its line.try {  tooEarly();} catch (e) {  console.log('expression before its line:', e.constructor.name);}const tooEarly = () => 'never reached';console.log('ok: three-ways');

The first line of output is the hoisting proof: calculateDamage is called before it is written and works. The tooEarly block is the same experiment with an arrow, and it throws, because an arrow in a const is a variable like any other. In between, Math.max(0, attack - defense) is a pattern you will reuse all week: a floor, so damage never goes negative. And oops(2, 3) is undefined. Hold on to that; it comes back in Where it breaks.

2. The rings, walked by the engine#

Global, function and block scope; the outward walk; shadowing; and the var leak seen from the scope side.

JSscope.js
'use strict';let worldName = 'Aincrad';              // global scope function enterDungeon() {  let dungeonLevel = 5;                 // function scope  console.log('inside the function:', worldName, dungeonLevel);   if (dungeonLevel > 3) {    let boss = 'Dragon';                // block scope    const loot = 'Legendary Sword';    console.log('inside the block:', boss, loot, dungeonLevel, worldName);  }   console.log('after the block, boss is', typeof boss);  // "undefined": the ring closed  return dungeonLevel;}enterDungeon();console.log('outside the function, dungeonLevel is', typeof dungeonLevel); // Lookups walk outward: an inner function can see and change an outer variable.let kills = 0;function slay() { kills++; }slay(); slay();console.log('kills seen from outside:', kills); // Shadowing: an inner declaration with the same name hides the outer one.let potion = 'minor';{  let potion = 'greater';  console.log('inside:', potion);}console.log('outside:', potion); // var ignores the block ring. This is the leak from Session 3.1, seen from the scope side.function varProblem() {  if (true) {    var leaked = 'I escaped the block!';  }  return leaked;}console.log(varProblem());console.log('ok: scope');

The demo uses typeof to probe for boss and dungeonLevel from outside their rings, because a bare console.log(boss) there would throw and stop the file; typeof on a name that does not exist is the one safe way to ask. In your own file, do it the loud way once: write console.log(boss) after the block, read the ReferenceError, and delete the line. The slay function is the outward walk doing useful work, and potion is shadowing: an inner declaration with the same name hides the outer one for the length of the block, which is legal, occasionally handy, and usually a sign to pick a different name.

3. Backpacks#

The three closures from fig. 3, the loop bug that let was invented to fix, and the shape the lab's mana pool will take. Part 4 uses a for loop before 3.4 teaches loops in full; for today, read for (let j = 0; j < 3; j++) as "run the body three times, with j counting 0, 1, 2".

JSclosures.js
'use strict';// 1. A counter that remembers. `count` lives in the backpack, not in the outside world.function createCounter() {  let count = 0;  return function () {    count++;    return count;  };}const hitCounter = createCounter();console.log(hitCounter(), hitCounter(), hitCounter());const comboCounter = createCounter();          // a brand-new backpackconsole.log('combo:', comboCounter(), '| hits still:', hitCounter());console.log('count from outside:', typeof count); // 2. A factory: the argument is captured, and each returned function keeps its own.function createGreeter(greeting) {  return (name) => `${greeting}, ${name}!`;}const sayHello = createGreeter('Hello');const sayYo = createGreeter('Yo');console.log(sayHello('Naruto'), '|', sayYo('Luffy')); // 3. A damage multiplier: configure once, cast many times.function createAttack(baseDamage) {  return (multiplier) => baseDamage * multiplier;}const fireball = createAttack(50);console.log(fireball(1), fireball(2.5)); // 4. The classic loop bug, and why let fixed it.const withVar = [];for (var i = 0; i < 3; i++) withVar.push(() => i);const withLet = [];for (let j = 0; j < 3; j++) withLet.push(() => j);console.log('var:', withVar.map((f) => f()), '| let:', withLet.map((f) => f())); // 5. The lab's shape: a closure that returns an object of methods sharing one secret.function createManaPool(maxMana) {  let currentMana = maxMana;  return {    cast(cost) {      if (cost > currentMana) return 'not enough mana!';      currentMana -= cost;      return currentMana;    },    rest() { currentMana = maxMana; return currentMana; },    getStatus() { return `${currentMana}/${maxMana} mana`; },  };}const pool = createManaPool(100);console.log(pool.cast(30), pool.cast(50), pool.cast(40), pool.getStatus());console.log(pool.rest(), pool.getStatus(), '| currentMana from outside:', pool.currentMana);console.log('ok: closures');

Part 4 is the most famous closure bug in the language, and it is worth a minute. The for (let j = 0; j < 3; j++) line is a counting loop: start j at 0, run the body while j < 3, add one after each pass; loops get their full introduction in 3.4, and this is the only one you need today. With var, there is one i for the whole loop, every arrow carries the same backpack, and by the time anyone opens it the loop has finished and i is 3. With let, each pass through the loop gets its own j, so each arrow carries its own backpack with its own value. That is the actual reason let has the scoping rules it has. Part 5 is a closure wearing an object: three methods, one shared currentMana, and pool.currentMana from outside is undefined because the mana was never a property. That shape is the lab.

4. Arrays, mutating and not, and the combo chain#

Everything from fig. 4: the mutating methods, the copying ones, and the four that take a function.

JSarrays.js
'use strict';const party = ['Warrior', 'Mage', 'Healer', 'Thief'];console.log(party[0], party[party.length - 1], party.at(-1), party.length); // Mutating methods change the array you already have.party.push('Ranger');party.pop();party.unshift('Tank');party.shift();party.splice(1, 1, 'Dark Mage');            // at index 1, remove 1, insert oneconsole.log('after mutation:', party); // Non-mutating methods hand back something new and leave party alone.const frontline = party.slice(0, 2);const combined = party.concat(['Bard']);console.log(frontline, combined, party.includes('Dark Mage'), party.indexOf('Healer'));console.log('party unchanged:', party); const heroes = [  { name: 'Goku', power: 9001, class: 'Warrior' },  { name: 'Naruto', power: 7500, class: 'Warrior' },  { name: 'Sailor Moon', power: 8200, class: 'Mage' },  { name: 'Deku', power: 6800, class: 'Warrior' },  { name: 'Jotaro', power: 8500, class: 'Fighter' },]; // forEach: do something per element. Returns undefined, so it ends a chain.heroes.forEach((h) => console.log(`${h.name}: Power Level ${h.power}`)); // map: same length, new shape.console.log(heroes.map((h) => h.name));console.log(heroes.map((h) => `${h.name} (${h.power > 8000 ? 'OVER 8000!' : h.power})`)); // filter: fewer or equal, same shape.console.log(heroes.filter((h) => h.power > 8000).map((h) => h.name));console.log(heroes.filter((h) => h.class === 'Warrior').length, 'warriors'); // reduce: one value. Always give it the starting value.const totalPower = heroes.reduce((sum, h) => sum + h.power, 0);console.log('total power:', totalPower); // Chaining: fig. 4, run by the engine.const strongTotal = heroes  .filter((h) => h.power > 8000)  .map((h) => h.power)  .reduce((sum, p) => sum + p, 0);console.log('strong total:', strongTotal); // Highest with reduce, and the honest check that nothing was mutated.const strongest = heroes.reduce((best, h) => (h.power > best.power ? h : best));console.log('strongest:', strongest.name, '| heroes still has', heroes.length);console.log('ok: arrays');

Two patterns to take away. The chain in strongTotal is fig. 4 line for line: five objects in, three after filter, three numbers after map, one number after reduce. Read it top to bottom like a sentence. And the last reduce, strongest, is the comparison shape: the accumulator is not a running total but the best candidate so far, and each pass keeps either the candidate or the newcomer. Notice that it is the one reduce on the page without a starting value. It works here because heroes has five elements, so the first one becomes the starting candidate. On an empty array it would throw, which is costume five below. There is a third reduce shape, the one that counts how many of each kind there are, and it waits for Session 3.3 so that you can build it slowly with a prediction in front of you.

5. The hero roster, shaped#

Not a run block. This is the shape the second exercise asks for, so you can see the whole file before you write it.

JS
const createHero = (name, level, cls, attack, isActive = true) => ({ name, level, class: cls, attack, isActive }); const roster = [  createHero('Aria', 12, 'Mage', 35),  createHero('Bron', 9, 'Warrior', 28, false),  // ... at least eight]; const getActiveHeroes = (r) => r.filter((h) => h.isActive);const getHeroNames = (r) => r.map((h) => h.name);const getTotalAttack = (r) => r.reduce((sum, h) => sum + h.attack, 0);const findStrongest = (r) => r.reduce((best, h) => (h.attack > best.attack ? h : best));

Notice the parentheses around the object in createHero: => ({ ... }). Without them, the braces would be read as a function body and the arrow would return undefined. That is costume three in the section below, and you have now seen it before it bit you.

Where it breaks#

Your turn#

The quest#

You descend into the Arcane Library, a vast underground chamber lined floor to ceiling with glowing tomes. Each spell is catalogued by element, mana cost and destructive potential. The Head Librarian hands you a quill. "Organise this mess, apprentice. And while you are at it, build me a mana pool tracker. I am tired of running out mid-battle."

The Spell Book, part one. Create spellbook.js in ~/projects/js-fundamentals and bring order to the Archives. The book is finished over two sessions. Today it gets its spells, its mana pool, and its first two queries; Session 3.3 adds the other four and pushes the finished file.

  1. An array of at least ten spell objects with name, element (fire, ice, lightning, earth, dark, light), manaCost, damage and level (the level required to cast it). Give every spell all five properties, spelled exactly the same way each time; the queries next session depend on it.
  2. The first two query functions, each one line if you can manage it: getSpellsByElement(spells, element) returns the spells of that element, and getAffordableSpells(spells, currentMana) returns the spells whose manaCost is at or below currentMana. Both use filter. Both hand back a new array and leave spells alone; log spells.length after calling them to prove it.
  3. A closure, createManaPool(maxMana), that returns an object with cast(manaCost) (spends mana and returns what is left, or "not enough mana!"), rest() (restores to max) and getStatus() (current and max as a string). currentMana must be a variable in the closure, never a property on the object.
  4. Test the pool: cast three spells, check status, rest, check status again. Then prove the privacy: log pool.currentMana and show it is undefined.

Hints that save an hour: write the spells first and log them, so every function has real data to run on; a filter callback is a question with a yes-or-no answer, so (s) => s.element === element is the whole body; and if a query ever returns [undefined, undefined, ...], you are wearing costume three.

Run it with node spellbook.js from inside ~/projects/js-fundamentals. The last lines should be the pool's casts, its status, its status again after rest(), and undefined for pool.currentMana. Commit it (git add spellbook.js and git commit -m "spellbook: spells, mana pool, two queries") but do not worry about the push yet; the file is pushed when it is finished next session.

Stretch goals, in order of difficulty: castSpell(spellbook, spellName, manaPool) that uses getSpellsByElement or a filter of its own to find the spell by name, checks the pool, and either casts it and returns the damage or explains why not; and a levelUp closure that tracks a character's level and reports which spells unlocked as it rises. Leave map and reduce queries for next session; they are the proving grounds.

Side quests#

Four pages in the library go deeper than one session can. Read them in this order; each one is a short reference sheet with code that runs in the page.

Side quests from this session

  • Closures, scope & hoistingThe reference sheet for today: scope, the backpack, the classic loop, and function hoisting as runnable asserts.
    Languages
  • ArraysEvery array method in one place, mutating versus copying, 2D grids, and the aliasing trap behind the shallow-copy demo.
    Languages
  • Objects & destructuringObjects as records, reading nested keys, and the copy table. The destructuring and spread half is Session 3.3 work; read that part when you get there.
    Languages
  • Sorting (comparators)The comparator function sort wants, why sort mutates, and how to sort by two keys without destroying the input.
    Languages

Before next session#

Three things, in this order.

First, finish part one of the Spell Book if it did not get done in the session: ten spells, the mana pool, and the two filter queries, committed in ~/projects/js-fundamentals. It is the artifact for this session, and Session 3.3 opens that exact file and adds to it. Nothing is running between sessions; node exits when the file ends.

Second, write three arrow functions of your own in the console: one single-expression, one with braces and an explicit return, and one that returns an object literal in parentheses. Thirty seconds each, and costume two and three will never catch you again.

Third, read MDN's Closures page, the whole thing. It is one of the best explanations on the web, and you now have the backpack picture to hang it on. When it reaches the loop example, you will recognise it.

Session 3.3 is called The Proving Grounds, and it is a different kind of session: fewer new ideas, more of your own hands. It teaches the three dots this page skipped, the honest rules for copying, and the counting reduce, each with a prediction you write before you press Run. Then it hands you three small problems on your own spellbook with no scaffolding, and a slower route back here if they do not go well. The four remaining queries, map, reduce, the comparison reduce and the counter, finish the book there. Objects, the class keyword and the word this wait for Session 3.4, and createManaPool is already the shape they will explain.

Say it once more without reading it. If it comes out in your own words, the Archives are in order.

See also