Without notes, state yesterday’s main idea and one unresolved question.
Scope, closures, and modules
JavaScript Foundations II and the Browser
Objective
Control where values are visible and split code into files.
Events are interrupts, promises represent future results, and the DOM is the browser’s live model of the page.
- block and function scope
- closures at a practical level
- export and import
Why this matters
Every file you have written so far has been one file, and every name in it was visible to every other line. That stops working around a hundred lines, when two parts of the program accidentally share a name and quietly overwrite each other.
Today you learn the two tools that fix it: scope, which controls where a name is visible, and modules, which split code across files and share only what you choose. By the end of the hour your inventory project is four files that import from each other, with nothing leaking into the global space.
Block scope and function scope
A scope is a region of code in which a name exists. Ask "can this line see that name?" and the answer is decided by scope, not by what looks nearby on screen.
let and const are block-scoped. A block is any pair of braces { } — the body of an if,
a loop, or a function. A name declared inside a block does not exist outside it.
if (true) {
let inner = "block only";
console.log(inner);
}
console.log(inner);
block only
ReferenceError: inner is not defined
That error is not a failure; it is the guarantee working. inner was never meant to escape.
Functions create a scope too, and they can see outward. A function body can read names from the scope that surrounds it, and from the scope surrounding that, all the way up. The reverse is never true: the outside cannot see in.
const threshold = 10;
function isLowStock(item) {
const label = item.name; // visible only inside this function
return item.quantity < threshold; // reads outward, and that is allowed
}
console.log(isLowStock({ name: "NE555", quantity: 3 })); // true
console.log(label); // ReferenceError: label is not defined
The old var declaration you were warned off on Day 15 is function-scoped, not block-scoped: a
var inside an if leaks to the whole enclosing function. That mismatch is why let and const
exist. Keep using them.
Shielded cans and exposed traces
A shielded section on a board contains its signals: nothing inside couples out, though the section can still take a reference voltage in from the board. A scope is that shield. Anything declared inside stays inside; anything on the outer plane can be read inward. A global variable is a bare trace across the whole board — every stage can touch it, and when the signal is wrong you have no idea which stage did it.
Closures, practically
When you return a function from another function, the returned function keeps access to the scope it was born in — even after the outer function has finished. That combination of a function plus the surrounding variables it remembers is a closure.
function makeCounter(startAt) {
let count = startAt;
return function next() {
count = count + 1;
return count;
};
}
const ticket = makeCounter(0);
console.log(ticket()); // 1
console.log(ticket()); // 2
const other = makeCounter(100);
console.log(other()); // 101
console.log(ticket()); // 3
Two facts to take from that output. First, count survives between calls — ticket() returns 1,
then 2, then 3 — even though makeCounter returned long ago. Second, other has its own
count: each call to makeCounter creates a fresh scope. And count itself is unreachable from
outside; the only way to change it is through the function that was given the key.
The everyday use is making configured functions. Day 22's array methods take a callback, so a closure is the tidy way to build one with a setting baked in:
function makeLowStockCheck(threshold) {
return (item) => item.quantity < threshold;
}
const isCritical = makeLowStockCheck(5);
const components = [{ id: "R1", quantity: 42 }, { id: "U1", quantity: 3 }];
console.log(components.filter(isCritical)); // [ { id: 'U1', quantity: 3 } ]
A peripheral with internal registers
A timer peripheral holds its own count register. You cannot reach in and write it directly; you talk to it through its pins, and it remembers its value between your accesses. Two timers on the same chip each keep their own count. A closure is exactly that: private state, reachable only through the function you were handed.
Modules: export and import
A module is one file treated as a unit of code. Modules help with organizing code into
explicit units and sharing only what each unit chooses to share. Two things follow. Everything declared at the top of a module file is private to that file by
default — it is not a global. And anything you mark with export becomes available to other
files that import it.
// calc.js
export function totalValue(items) {
return items.reduce((sum, item) => sum + item.quantity * item.unitPrice, 0);
}
export function lowStock(items, threshold) {
return items.filter((item) => item.quantity < threshold);
}
// main.js
import { components } from "./data.js";
import { totalValue, lowStock } from "./calc.js";
Three rules the browser enforces, and each one is a common first-time failure:
- The path needs a
./(or../) prefix and the.jsextension.import ... from "calc.js"fails in a browser;"./calc.js"works. - The names in the braces must match the exported names exactly, including case.
importlines go at the top of the file. They are resolved before any of your code runs.
The names in braces are named exports. A file may also have one export default, imported
without braces as import anyName from "./calc.js". Named exports are clearer — the name in the
import matches the name in the source — so use those.
A workshop with a service counter
A module is a workshop with one counter facing the corridor. Whatever the workshop does inside
is its own business; the only things anyone else can take are the items placed on the counter.
export is putting something on the counter. import is walking up and asking for it by name.
Walkthrough
Build the four-file project. In ~/fullstack-journey/inventory-app, create data.js, calc.js,
render.js, main.js, and index.html.
// data.js
export const components = [
{ id: "R1", name: "Resistor 10k", category: "resistor", quantity: 42, unitPrice: 0.02 },
{ id: "C1", name: "Capacitor 100nF", category: "capacitor", quantity: 8, unitPrice: 0.05 },
{ id: "U1", name: "NE555 timer", category: "ic", quantity: 3, unitPrice: 0.45 },
];
// render.js
export function formatLine(item) {
return `${item.id} ${item.name} — ${item.quantity} left`;
}
Put the calc.js from above in place, then wire it together:
// main.js
import { components } from "./data.js";
import { totalValue, lowStock } from "./calc.js";
import { formatLine } from "./render.js";
console.log("Total value:", totalValue(components).toFixed(2));
console.log(lowStock(components, 10).map(formatLine).join("\n"));
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Inventory</title>
</head>
<body>
<h1>Inventory</h1>
<script type="module" src="./main.js"></script>
</body>
</html>
type="module" is what makes import legal in the browser. You load only main.js; the browser
follows its imports and fetches the rest itself.
Do not open this file by double-clicking it
Modules are fetched over the network, and a file:/// page has no origin the browser trusts, so
the console reports a CORS error mentioning origin 'null' and nothing runs. Serve the folder
instead, using the command from Day 5:
cd ~/fullstack-journey/inventory-app
npx serve
Open the http://localhost:... address it prints, then open the Console (Day 6).
Total value: 2.59
C1 Capacitor 100nF — 8 left
U1 NE555 timer — 3 left
Checkpoint
In the Console, type components and press Enter. You get ReferenceError: components is not defined — proof the module's names never became globals. That error is the deliverable working.
Your turn
Build a browser project with working module imports and no global variables.
- Get the four files above running and confirm the three lines appear in the Console.
- Move your Day 21 calculator functions (series resistance, parallel resistance, wattage) into a
new
calc-resistors.js, oneexport functioneach. Import and call them frommain.js. - Add a
categoryTotal(items, category)tocalc.jsusingfilterandreducefrom Day 22. Export it, import it, and print one category total. - In
render.js, addformatReport(title, lines)that returns a heading plus the lines joined by newlines. Keep everyconsole.loginmain.js—render.jsreturns strings, it does not print. - Deliberately break one import: change
"./calc.js"to"calc.js". Read the console error, then fix it. Break a name next: import{ totalvalue }. Read that error too, then fix it. - Prove the isolation. In the Console, type the name of any function you defined. Each should
report
is not defined.
You are done when
The page's Console shows your reports, every file exports what it shares, and no name from your code can be reached from the Console.
Tutor mode — while you work on step 2
Today's mode is tutor: ask for an explanation, a small example, then attempt it yourself. Never ask for the finished file, because a file you did not write is one you cannot debug.
"Trace variable scope through this code and identify what each function can access."
Predict the answer for each function before you read the reply, then compare.
Common pitfalls
- Opening the HTML from the filesystem. Modules need
http://. If nothing runs and the console mentions CORS ororigin 'null', you skippednpx serve. - Forgetting
type="module". A plain<script src="./main.js">givesSyntaxError: Cannot use import statement outside a module. - Exporting nothing and wondering why the import is empty.
exportmust be on the declaration you want shared. Noexport, no counter, no access. - Declaring the same name in two scopes and expecting the inner one to update the outer. It does not — the inner declaration is a separate variable that shadows the outer one for that block.
Verify it yourself
Open today's reference, MDN's Dynamic scripting with JavaScript, and find its pages on function scope, closures, and JavaScript modules.
- MDN states that module code runs in strict mode automatically, and that modules are deferred until the page is parsed. Find both statements and write down one consequence of each.
- This lesson recommended named exports over
export default. Find MDN's description of default exports and write one sentence on when a default might actually be the better choice.
Add both answers as comments at the top of main.js. Reading the specification for the rule behind
a habit is how the habit becomes a decision.
The hour
- 0–5 min Recall
- 5–20 min Learn
Read only the listed concept notes and official reference sections needed today.
- 20–48 min Build
Split calculator logic, inventory data, and presentation into separate ES modules.
- 48–55 min Explain and verify
Run the result, inspect evidence, and explain the data/control flow in your own words.
- 55–60 min Quiz and commit
Complete the quiz, record one lesson, and commit the verified change when applicable.
What to hand in
A browser project with working module imports and no global variables.
Working with AI today
Ask for explanations, analogies, questions, and hints. Do not request a complete finished solution first.
Trace variable scope through this code and identify what each function can access.
References
End-of-day quiz
Explain-back gate
Pass the quiz above to unlock completion.
Quiz + explain-back checks required.