JavaScript module 2: the hard parts, made friendly
Fourteen sessions for the notions that separate "I tinker" from "I program": where variables live, functions that remember (closures), the three array-transforming methods (map, filter, reduce), destructuring, well-behaved errors, JSON and dates. Each is first explained with a tiny example, then practised until it flows. Method: 20 minutes of theory, 30 of practice, 10 of recap.
Day 15 — Scope: where a variable lives
Goal: predict where a variable is visible, and understand "shadowing".
Theory (20 min)
A variable's scope is the zone of code where its name is visible. With let and const, the rule is simple: a variable lives in the block that saw it born — the block being whatever sits between braces: a function, an if, a loop.
const global = "everywhere";
function trial() {
const local = "here only";
if (true) {
const finer = "inside the if";
console.log(global, local, finer); // all three are visible
}
console.log(global, local); // finer no longer exists here
}
// here, neither local nor finer exist
Reading goes from the inside out: in a block, an unknown name is looked up in the parent block, then the grandparent, up to the top of the file — then ReferenceError. A direct consequence: the same name can exist on two levels. let total = 0; at the top, then let total = 5; inside a function: inside, total means the 5, the 0 is shadowed. Legal, but a bug source: when a name is shadowed, read twice.
Why this is good news: block scope means two functions can each have their own total without stepping on each other, and a loop variable for (const item of groceries) vanishes with the loop. Variables "lingering everywhere" are a design flaw, not a convenience.
Without running it: let x = 1; if (true) { let x = 2; } console.log(x);
- 1
- 2
- ReferenceError
Answer
1: the if's x is another variable, born and dead inside the block.
Practice (30 min)
- Retype the course example into
scope.js, run it, then move the three-variable console.log out of the if: read the ReferenceError and note which name is missing. - Write two functions
countA()andcountB(), each declaringlet total = 0;, incrementing and returning it. Call them: each has ITS total. Success criterion: both print 1, independently. - Create shadowing:
const unit = "cm";at the top, then inside a functionconst unit = "px";and a console.log in each zone. Predict, run, note. - Test the loop variable:
for (const c of [1, 2]) {}thenconsole.log(c)— ReferenceError, the loop took everything with it.
Agent tips: "in this file, list each variable with the exact zone where it is visible": an agent draws this map well. Trap: it can miss a shadowing when the file is long — give it the whole file, not an excerpt. Proof: a ReferenceError triggered on purpose confirms the boundary.
Recap (10 min)
Three lines: the block rule, the outward lookup, shadowing. Without notes: write an if that declares a variable and prove it does not exist after the if.
Day 16 — Closures: a function that remembers
Goal: understand and write a closure: a function that keeps access to the variables of its birthplace.
Theory (20 min)
A function can be returned by another function. And here is the fact that made the language's reputation: the returned function carries along the variables of the function that created it, even after that one has finished. This bundle "function + its remembered variables" is called a closure.
function makeCounter() {
let total = 0; // private variable: unreachable from outside
return function () {
total++;
return total;
};
}
const counter = makeCounter();
counter(); // 1
counter(); // 2
counter(); // 3
Walk it by hand, it is the only way to feel it: makeCounter() creates total, returns the small function, and finishes. Yet total is not dead: the small function holds it. Each call pushes it up. And total is private: impossible to touch from outside, only the function reaches it. This is the basic tool for building protected state without classes.
A second example to know, the function factory:
const times = n => x => x * n;
const triple = times(3);
triple(5); // 15
times(3) returns a function that "remembers" that n is 3. Three quarters of the closures you will write look like one of these two patterns: the counter (private state) or the factory (a configured function).
Write makeGreeting(formula), returning a function: called with a name, it returns « formula name! ».
function makeGreeting(formula) {
}
Answer
function makeGreeting(formula) {
return function (name) {
return `${formula} ${name}!`;
};
}Practice (30 min)
- Retype the counter into
closure.js, run it. Then create a SECOND counter:const other = makeCounter();Call both alternately. Success criterion: each keeps its own total — two closures, two memories. - Prove the private life:
counter.totaland any direct read fail (undefined). The only path to total is a call. - Retype the
timesfactory, createdoubleandtimes10, test them. - Pass the boxed
makeGreetingexercise. - In the notebook, explain in your own words why
totalsurvives the end ofmakeCounter.
Agent tips: "explain this closure line by line, then predict the next five calls": a good cross-check exercise. Trap: an agent can describe the mechanism correctly and still get the fifth call wrong. Proof: you run the five calls and compare.
Recap (10 min)
Three lines: what a closure carries, the two patterns (counter, factory), the offered private life. Without notes: rewrite makeCounter from memory and count to 3.
Day 17 — map: transforming a list
Goal: transform every element of an array with map, without a hand-written loop.
Theory (20 min)
map applies a function to each element of an array and returns a new array of the results, of the same length:
const prices = [10, 20, 30];
const withTax = prices.map(p => p * 1.2);
// [12, 24, 36]
Read prices.map(p => p * 1.2) as: "for each p of prices, yield p × 1.2". The function passed as argument is called by map, once per element, with the current element. It is a higher-order function: a function taking a function as a parameter. You saw on day 12 that a function is a value; map is that fact's first great use.
Three things to engrave: map does NOT modify the original array (check: prices is intact afterwards); the result ALWAYS has the same length as the input; the function also receives the index as a second parameter (prices.map((p, i) => ...)) for the rare cases where position matters. If you write a loop doing result.push(transform(element)), it can become a map.
Write initials(names): for an array of full names like "Ada Lovelace", return the initials "AL". Hint: map, split(" "), and first letters.
function initials(names) {
}
Answer
function initials(names) {
return names.map(name => name.split(" ").map(word => word[0]).join(""));
}Practice (30 min)
- In the REPL:
[1, 2, 3, 4].map(n => n ** 2), then["ada", "grace"].map(s => s.toUpperCase()). Each time, check that the original array is intact. - With
const basket = [{ name: "apples", price: 3 }, { name: "rice", price: 2 }];, produce the array of prices only:basket.map(a => a.price)→ [3, 2]. Then the array of names. - Turn the basket into sentences:
basket.map(a => `${a.name}: $${a.price}`). Success criterion:["apples: $3", "rice: $2"]. - Use the index:
["a", "b", "c"].map((letter, i) => `${i + 1}. ${letter}`). - Pass the boxed
initialsexercise.
Agent tips: "rewrite this for loop with map": an agent does this conversion very well. Trap: it may slip in a filter or forEach "to optimise" — demand the minimal conversion. Proof: the new code produces an identical array on your data.
Recap (10 min)
Three lines: what map takes and what it returns, the source array's immutability, the index parameter. Without notes: turn [2, 4, 6] into [1, 2, 3] with a single map.
Day 18 — filter: keeping what matters
Goal: select the elements of an array with filter and a predicate.
Theory (20 min)
filter walks an array and keeps only the elements for which your function returns true. The function is called a predicate: it takes an element, answers true or false.
const ages = [12, 25, 17, 34];
const adults = ages.filter(age => age >= 18);
// [25, 34]
The result has at most the input's length — from zero elements to all of them. Like map, filter does not touch the original. And like map, it replaces a loop: if you write if (condition) result.push(element) in a for, that is a filter in disguise.
The natural combination is map AFTER filter: first keep what matters, then transform it:
const basket = [
{ name: "apples", price: 3 },
{ name: "coffee", price: 12 },
{ name: "rice", price: 2 }
];
const pricey = basket.filter(a => a.price > 5).map(a => a.name);
// ["coffee"]
Chaining reads left to right like a conveyor belt: the array goes in, each method returns another one. We will practise it fully on day 25; today, the key is a well-written predicate: a clear boolean expression, no side effects.
Write evens(numbers), keeping only the even numbers.
function evens(numbers) {
}
Answer
function evens(numbers) {
return numbers.filter(n => n % 2 === 0);
}Practice (30 min)
- In the REPL:
[5, 12, 8, 130, 44].filter(n => n > 10). Check that the original is intact. - With the course's basket, keep the items under $5, then produce their names in uppercase (filter then map).
- Filter
["ada", "", "grace", ""]to drop empty strings — remember day 9's falsy:.filter(s => s.length > 0), or shorter.filter(Boolean)? Test both and explain why the second works. - Success criterion: for the basket, you get
["coffee"]for "over $5" and["APPLES", "RICE"]for "under $5, uppercased". - Pass the boxed
evensexercise.
Agent tips: "here is my array of objects and the expected list; write the filter": with the real data pasted, an agent aims true. Trap: without the data excerpt, it invents key names. Proof: the result compared element by element with the expected list.
Recap (10 min)
Three lines: what a predicate is, the map/filter difference, the filter-then-map order. Without notes: from [3, 10, 7, 20], keep those > 8 then double them: [20, 40].
Day 19 — reduce: folding everything into one value
Goal: fold an array into a single value with reduce, without getting the initial value wrong.
Theory (20 min)
reduce is the most powerful of the three and the least intuitive: it folds an array into a single value (a number, an object, another array…) by passing an accumulator from element to element.
const prices = [10, 20, 30];
const total = prices.reduce((acc, p) => acc + p, 0);
// 60
Walk it: the accumulator acc starts at the initial value (0, reduce's second argument). First element: acc=0, p=10, the function returns 10, which becomes the new acc. Second: acc=10, p=20 → 30. Third: acc=30, p=30 → 60. The last acc is the result.
Two traps dominate. First: forgetting the initial value. Without it, reduce takes the first element as the starting acc — and on an empty array, it throws a TypeError. Always set the initial value; it also states the shape of the result (0 for a sum, {} for a count, [] for a list). Second: the function must return the accumulator each turn; one forgotten return and everything becomes undefined.
// counting occurrences: the initial value is an empty object
const words = ["a", "b", "a"];
const count = words.reduce((acc, word) => {
acc[word] = (acc[word] ?? 0) + 1;
return acc;
}, {});
// { a: 2, b: 1 }
Write countAll(items), returning an object counting each element's occurrences, with reduce.
function countAll(items) {
}
Answer
function countAll(items) {
return items.reduce((acc, el) => {
acc[el] = (acc[el] ?? 0) + 1;
return acc;
}, {});
}Practice (30 min)
- In the REPL, redo the course's sum by walking it in the notebook, column by column (acc, p, return). Then change the initial value to 100: 160. Understand why.
- Compute the maximum of
[4, 18, 7, 12]with reduce:(acc, n) => n > acc ? n : acc, initial value… which one? Think about the trap if all numbers are negative. - Trigger the TypeError:
[].reduce((a, b) => a + b)without an initial value. Read the message, it says it all. - Success criterion: your occurrence count on
["a", "b", "a"]gives{ a: 2, b: 1 }, and on[]gives{}without an error. - Pass the boxed
countAllexercise.
Agent tips: "walk this reduce turn by turn in a three-column table": excellent for checking your own walkthrough. Trap: on an object accumulator, the agent may skip a step. Proof: a console.log(acc) inside the function, run by you, shows each turn.
Recap (10 min)
Three lines: the accumulator, the initial value and its double role, the mandatory return. Without notes: write the product of [2, 3, 4] with reduce → 24.
Day 20 — Destructuring, spread and rest
Goal: extract values from objects and arrays in one line, and use the three dots in both directions.
Theory (20 min)
Destructuring extracts an object's properties (or an array's elements) directly into variables:
const book = { title: "1984", author: "Orwell", pages: 328 };
const { title, author } = book;
// title is "1984", author is "Orwell" — two consts in one line
const [first, third] = ["a", "b", "c"];
// first: "a", third: "b" — arrays destructure by position
On objects, the names must match the keys; you can rename (const { title: t } = book;) and give a default (const { rating = 0 } = book; — the default only applies if the value is undefined). The form that matters most in practice: destructured parameters — function show({ title, author }) receives an object and extracts only what it needs. No more options.a, options.b, options.c at the top of a function.
The three dots ... have two faces. As spread, they unpack: const copy = { ...book, pages: 400 }; creates a new object with all of book, except pages replaced — THE modern way to "modify" without mutating. As rest, they gather: const [head, ...tail] = [1, 2, 3]; gives head=1, tail=[2,3]. Same syntax, two directions: spread when building, rest when receiving.
What is const { a = 5 } = { a: 0 }; a?
- 5
- undefined
- 0
Answer
0: the default only applies when the property is undefined — 0 is a real value.
Practice (30 min)
- Destructure
const user = { name: "Ada", city: "London", admin: true };into three variables, then renaming name to nickname, then with a defaultlanguage = "en". - Write
function label({ name, city })returning« name (city) »and call it with user. - Do a "modification without mutation":
const user2 = { ...user, admin: false };Check that user.admin is still true. Success criterion: user is intact, user2 has admin at false. - Spread two arrays into one:
[...[1, 2], ...[3, 4]]. Then gather:const [a, ...rest] = [1, 2, 3, 4]; - A trap to know: a spread copy is shallow — sub-objects stay shared. Check:
const o = { p: { x: 1 } }; const c = { ...o }; c.p.x = 9; o.p.x;gives 9.
Agent tips: "rewrite this function to take a destructured options object": an agent does this modernisation well. Trap: it may destructure too early and break existing calls. Proof: old calls and new calls give the same result — test both.
Recap (10 min)
Three lines: destructuring by key vs by position, default only if undefined, spread unpacks / rest gathers. Without notes: swap two variables with destructuring ([a, b] = [b, a]) and copy an object changing one key.
Day 21 — Errors: throw, try…catch
Goal: raise an error yourself, catch it properly, and know when NOT to catch.
Theory (20 min)
So far, errors happened to you. Today you create them: throw interrupts execution and raises an error, try…catch catches it at the level that knows what to do with it.
function divide(a, b) {
if (b === 0) {
throw new Error("division by zero");
}
return a / b;
}
try {
const r = divide(10, 0);
console.log(r);
} catch (err) {
console.log("caught error:", err.message);
}
The mechanics: throw makes the error bubble up from function to function until the first catch — or until the crash if nobody catches it. The optional finally block runs in all cases (useful to close a file, restore a state). Always raise an Error (new Error("...")), not a string: an Error carries the call stack, priceless when debugging.
The discipline that sets good code apart: throw early, catch late. The function that detects the impossibility (invalid parameter, missing data) throws immediately, with a message saying what it expected and what it got. Catching happens at the level that can decide: show a message to the user, retry, give up cleanly. Catching to "silence" (an empty catch { }) is the worst reflex: it turns a visible bug into an invisible one. A program failing in silence is far worse than a program crashing noisily.
Write convert(text), returning Number(text), but throwing an Error with the message "not a number" if the result is NaN. (The third check only makes sure the empty string does not crash.)
function convert(text) {
}
Answer
function convert(text) {
const n = Number(text);
if (Number.isNaN(n)) {
throw new Error("not a number");
}
return n;
}Practice (30 min)
- Retype
divideintoerrors.js, call it with 0 outside try: read the crash and its stack. Then inside a try…catch: the program survives and prints the message. - Success criterion:
node errors.jsprints "caught error: division by zero" and ends without an error stack. - Add a
finallythat prints "end of processing": observe it runs with AND without an error. - Write the bad reflex then repair it: an empty
catcharound code that crashes, observe the silence, put back the error's console.log. - Pass the boxed
convertexercise.
Agent tips: "which errors should this function throw, with which messages?": an agent enumerates edge cases well. Trap: it may wrap the whole body in a try "for safety". Proof: for each proposed throw, write the call that triggers it — if you cannot, the case does not exist.
Recap (10 min)
Three lines: throw bubbles up to the catch, why new Error and not a string, throw early / catch late. Without notes: write an age(x) function that throws if x is negative, and the call that catches it.
Day 22 — JSON: the web's exchange format
Goal: convert between JavaScript objects and JSON text, and know the format's limits.
Theory (20 min)
JSON (JavaScript Object Notation) is the text format in which the web exchanges data: API responses, configuration files, backups. It looks like a JavaScript object, but it is text, with strict rules: keys in double quotes, no trailing commas, no comments, no functions.
const object = { name: "Ada", age: 36 };
const text = JSON.stringify(object);
// '{"name":"Ada","age":36}' — a string, storable, sendable
const reread = JSON.parse(text);
// { name: "Ada", age: 36 } — a real object, usable again
stringify serialises (object → text), parse deserialises (text → object). Two practical details of stringify: a second argument set to null and a third to 2 produce indented text, readable by humans: JSON.stringify(object, null, 2). And parse is violent: malformed text throws a SyntaxError — on data coming from elsewhere, wrap parse in a try.
The limits to know: JSON cannot represent undefined (the property vanishes), functions (ignored), dates (stringify turns them into an ISO string, parse does not turn them back — you get text), NaN and Infinity (they become null). When data "loses a piece" after a JSON round trip, it is almost always one of these cases.
What does JSON.stringify({ a: undefined, b: 1 }) contain?
- '{"a":undefined,"b":1}'
- '{"b":1}'
- an error
Answer
Property a disappears: JSON has no undefined.
Practice (30 min)
- In the REPL, do the full round trip: an object with string, number, boolean, array, sub-object → stringify → parse → check a deep property.
JSON.stringify(reread) === textmust be true. - Success criterion:
rereadis not the same object asobject(it is a rebuilt copy) but matches it perfectly:reread.name === object.name. - Produce the indented version with the third argument, and print it: that is your future package.json's format.
- Trigger the failure:
JSON.parse("{name: 'Ada'}")(single quotes, unquoted key) — SyntaxError. Catch it in a try. - Observe the losses: stringify an object holding a Date, undefined, NaN and a function; parse; list what changed in the notebook.
Agent tips: paste a real (truncated) API response and ask "is this valid JSON, and which path leads to field X?". Trap: the agent may "repair" invalid JSON without telling you — require it to flag any modification. Proof: JSON.parse run at home settles it.
Recap (10 min)
Three lines: stringify/parse, the strict rules of JSON text, the four losses in transit (undefined, functions, dates, NaN). Without notes: serialise an object, print it indented, read it back.
Day 23 — Dates: Date and Intl
Goal: create, compare and format dates without pain — and without reinventing the calendar.
Theory (20 min)
Dates are the slippery topic of every language. JavaScript offers Date: an object representing ONE instant, stored as milliseconds since 1 January 1970 UTC (the "Unix timestamp").
const now = new Date();
const xmas = new Date("2026-12-25");
now.getTime(); // milliseconds since 1970 — comparable, computable
xmas > now; // comparisons work on instants
now.toISOString(); // "2026-11-04T10:23:45.123Z" — the standard text form
Three survival rules. (1) Store and compare timestamps or ISO strings, never local strings: "04/11/2026" means 4 November in France and 11 April in the United States. (2) getMonth() counts months from 0 to 11 — January is 0, the classic mistake. (3) Never hand-write calendar arithmetic (leap years, time zones, daylight saving): the edge cases will beat you.
To display a date to a human, Intl.DateTimeFormat does the job properly, with language and time zone as parameters:
const fmt = new Intl.DateTimeFormat("en-GB", { dateStyle: "long" });
fmt.format(new Date("2026-12-25"));
// "25 December 2026"
Differences are computed in milliseconds then converted: (b - a) / 86_400_000 gives days (the _ separator in long numbers is legal and welcome).
Practice (30 min)
- In the REPL: create today's date, print its timestamp with getTime, its ISO form with toISOString, and its localised version with Intl.DateTimeFormat.
- Compute your age in days: today's timestamp minus your birth date's timestamp, divided by 86_400_000, floored with Math.floor.
- Success criterion: the result is consistent with an online calculator; and
new Date("2026-01-15").getMonth()did give you 0 — note it. - Compare: which of two given dates is older? Write the function
older(a, b)returning the smaller one. - Format the same date in "en-GB", "en-US" and "ja-JP": three writings, one instant.
Agent tips: "format this timestamp for a British reader": the agent knows Intl. Trap: it may invent Intl options that do not exist. Proof: run it — an invalid option throws an immediate RangeError, ruthless and all the better for it.
Recap (10 min)
Three lines: timestamp vs display, getMonth from 0 to 11, why Intl and not hand-made formatting. Without notes: print today's date in long localised form, then the number of days until next 1 January.
Day 24 — find, some, every, sort: the other Swiss army knives
Goal: complete the array toolbox: find, test, sort.
Theory (20 min)
Beyond the map/filter/reduce trio, four methods answer daily questions:
find(predicate): the FIRST element passing the predicate, or undefined.users.find(u => u.id === 3)— lookup by key, the one you will write a thousand times.some(predicate): true if AT LEAST one element passes.ages.some(a => a < 18)— "is there a minor?"every(predicate): true if ALL pass.ages.every(a => a >= 0)— "are all values sane?"sort(comparator): sorts the array… in place (it mutates the original!) and, without a comparator, in ALPHABETICAL string order:[10, 9, 80].sort()gives[10, 80, 9]. The correct comparator for numbers:(a, b) => a - b(ascending) or(a, b) => b - a. To avoid mutation: copy first,[...t].sort(...), or usetoSorted(Node 20+, recent browsers).
And two useful poor relations: includes(value) for arrays of simple values (day 6), flat() which flattens arrays of arrays by one level, and flatMap(f) which does a map then a flat in one go — handy when each element produces a small list.
What does [40, 1, 5, 200].sort() give?
- [1, 5, 40, 200]
- [1, 200, 40, 5]
- [200, 40, 5, 1]
Answer
Alphabetical sort: "1" < "200" < "40" < "5". The comparator (a, b) => a - b is mandatory for numbers.
Practice (30 min)
- With
const users = [{ id: 1, name: "Ada" }, { id: 2, name: "Grace" }];, find user 2 with find, check the absence of 99 (undefined, no error). - Test some and every on
[3, 7, 21]: "all odd?" (every, true), "at least one even?" (some, false). - Reproduce the alphabetical-sort trap, fix it with the comparator, then with toSorted on a copy — check the original is untouched.
- Success criterion:
[40, 1, 5, 200].toSorted((a, b) => a - b)gives [1, 5, 40, 200] and the original has not moved. - flatMap:
["a b", "c d"].flatMap(s => s.split(" "))→ ["a", "b", "c", "d"].
Agent tips: "which array method for [a need described in one sentence]?": map/filter/find/some/every/reduce, the agent knows the map. Trap: it may suggest findIndex or reduce where some suffices — ask for "the simplest". Proof: the minimal example run in the console.
Recap (10 min)
Three lines: find returns an element or undefined, some/every return a boolean, sort mutates AND sorts as text by default. Without notes: sort [30, 4, 21] correctly both ways (with a copy).
Day 25 — Chaining methods: a data pipeline
Goal: compose map, filter, reduce into readable pipelines, and know when to stop.
Theory (20 min)
Every array method returns an array (except reduce, find, some, every), so they chain: one method's output is the next one's input. It is a pipeline, and it is modern JavaScript's style for processing collections:
const sales = [
{ product: "coffee", qty: 2, price: 12 },
{ product: "tea", qty: 1, price: 8 },
{ product: "coffee", qty: 3, price: 12 }
];
const coffeeRevenue = sales
.filter(s => s.product === "coffee")
.map(s => s.qty * s.price)
.reduce((acc, amount) => acc + amount, 0);
// 60
Reading: one line per stage, each stage does one thing, the result's name says what comes out. Three disciplines: (1) put the filter BEFORE the map when possible — no point transforming what you will discard; (2) each arrow function stays short and free of side effects — if it exceeds two lines, name it and pull it out; (3) know when to stop: an unreadable eight-stage pipeline is worth less than three well-named intermediate variables.
An honest cost note: each stage creates an intermediate array. For this path's collections (a few hundred elements), that is negligible — favour readability. The day you process millions of elements, you will rewrite as a loop; that day, you will know why.
Practice (30 min)
- Retype the sales example, run it: 60. Then swap filter and map (map first onto amounts) — notice it still works but the filter no longer has access to the product: order carries meaning.
- On
[" Ada ", "", " grace", "hopper "]: clean (trim), drop empties, uppercase, in three chained stages. Result: ["ADA", "GRACE", "HOPPER"]. - From
[{ name: "a", tags: ["x", "y"] }, { name: "b", tags: ["z"] }], produce the flat list of tags with flatMap. - Success criterion: the three pipelines return exactly the announced results, and each pipeline reads aloud like a sentence.
- Find in your module 1 files a for…of loop that builds an array: rewrite it as a pipeline.
Agent tips: "rewrite this processing as a filter/map/reduce pipeline, one stage per line": an agent excels at this reshaping. Trap: it may merge two stages "for performance" and kill readability. Proof: same data in, same result out, and each line does one thing.
Recap (10 min)
Three lines: chaining and its reading direction, filter before map, when to prefer intermediate variables. Without notes: write the pipeline "tax-included prices of items under $10, then their sum".
Day 26 — Mini-project: the scoreboard
Goal: assemble objects, arrays and pipelines into a complete results analyser.
Theory (20 min)
Today's project: an array of player scores, and four questions to answer with code. That is the exact shape of 80% of real JavaScript: a list of objects, questions, pipelines.
const players = [
{ name: "Ada", points: [12, 18, 7] },
{ name: "Grace", points: [20, 15] },
{ name: "Alan", points: [5, 9, 11, 3] }
];
The working method matters as much as the result: one question at a time, one function per question, each function checked on the data BEFORE moving to the next. The four questions: each player's total, the descending ranking, the best individual score of the match, the overall average. Each combines seen tools: map + reduce for totals, toSorted for ranking, flatMap + reduce for the record.
File structure: data at the top, functions in the middle, the four presentation console.logs at the bottom. And for the ranking, remember day 24: toSorted and a comparator, never bare sort.
Practice (30 min)
- Create
scores.jswith the course's data. WritetotalOf(player): the sum of their points with reduce. Check: Ada → 37. totals(): map over players, each element becomes{ name, total }. Check all three.ranking(): totals() sorted by descending total with toSorted. Check: Ada, Grace, Alan.record(): the largest number among all points (flatMap then reduce, or Math.max with spread). Check: 20.overallAverage(): the sum of all points divided by their count. Check: 100 / 9 ≈ 11.11.- Success criterion: the five functions print their results at the bottom of the file, and the numbers match hand computation.
Agent tips: "here are my data and my five hand-computed results; write the functions that reproduce them": giving expected results turns the agent into a checker. Trap: without these witness values, wrong-but-plausible code slips through. Proof: the five numbers, compared one by one.
Recap (10 min)
Three lines: the one-question-one-function method, the map+reduce pair for aggregating, why toSorted. Without notes: rewrite totalOf and ranking from memory.
Day 27 — Guided review
Goal: redo module 2's key moves, from memory, with the console as sole judge.
Theory (20 min)
No new notion. Re-read your notebook from days 15 to 26 and list what resisted you: for most people it will be the closure (day 16) and reduce (day 19) — the module's two bosses. Re-read those two sessions in full if their examples do not come back.
The module's honest test: can you, without notes, (1) write a function factory, (2) walk a reduce over three elements, (3) turn a list of objects into the answer to a business question ("the names of items in stock, by ascending price")? These three moves cover 90% of application JavaScript.
Without running it: [1, 2, 3].map(n => n * 2).filter(n => n > 2).reduce((a, b) => a + b, 0)
- 6
- 10
- [4, 6]
Answer
10: map gives [2, 4, 6], filter keeps [4, 6], reduce folds into 10. Always walk stage by stage.
Practice (30 min)
- Rewrite from memory: the counter closure (day 16), the occurrence count with reduce (day 19), the sales pipeline (day 25). Run each.
- Success criterion: all three run on the first or second try. Note what stuck — that is your re-reading list.
- Fresh exercise:
const words = ["cat", "dog", "rat", "goldfish"];— produce, in one pipeline, the total length of words longer than three letters. (Answer: 8.) - Fresh exercise: from an array of
{ city, temperature }objects you invent (5 cities), output the names of cities above 59°F… or 15°, uppercased, in alphabetical order. - Prediction then check:
const f = n => g => n + g; const add5 = f(5); add5(10)— write the answer first.
Agent tips: "generate five prediction exercises on map/filter/reduce with their answers in a foldable details": an agent is a good quiz machine. Trap: check every proposed answer in the console — one in twenty is wrong. Proof: the console, always the console.
Recap (10 min)
Three lines: the two reworked notions, your success rate on the memory exercises, what stays fragile. Tomorrow, module review.
Day 28 — Module 2 review
Goal: validate module 2 and prepare the great turn: asynchrony.
Theory (20 min)
The module's oral self-test: explain each of these sentences as if to a friend: "a function carries the variables of its birthplace", "map transforms, filter keeps, reduce folds", "the initial value states the result's shape", "sort sorts as text and mutates the original", "stringify forgets undefined and functions", "throw early, catch late". Each fuzzy sentence points back to its session.
Module 3 changes floors. So far, every line finished before the next began — synchronous code. But a real program waits: the network, a file, a timer. JavaScript has an elegant and unsettling answer to waiting: it never blocks, it comes back later. That is the event loop, and promises are its currency. Everything you learned about functions-as-values (days 12 and 16) becomes the main tool: a function you hand over so you can be called back.
Practice (30 min)
- Redo the module's four boxed exercises (makeGreeting, initials, evens, countAll) without reopening the answers: that is the honest measure.
- Success criterion: four green lights. Below that, redo the faulty session before module 3.
- Timed mock exam (20 min):
const orders = [{ client: "A", amounts: [10, 5] }, { client: "B", amounts: [20] }, { client: "C", amounts: [] }];Write the functions: total per client, active clients (total > 0), overall revenue, top-spending client. Answers: A:15, B:20, C:0; active A and B; revenue 35; client B. - Tidy up:
js/module-2/folder, notebook up to date.
Agent tips: "review my scores.js and find three readability improvements, without changing results": reviewing is an agent's best use at this stage. Trap: it may rename to its taste — the names are yours. Proof: the five results unchanged after applying.
Recap (10 min)
Three lines: the three most solid notions, the mock exam passed or not, what you expect from asynchrony. Well-earned rest: module 3 is the path's most surprising.
Found this useful? Tell us what to measure next.
Comments
No account, no tracker: a comment is read by a human before it appears.