5.3 — The scope chain, precisely
Lesson 3.5 gave you the picture: nested bubbles, a lookup ray shooting outward. It worked. But now you own execution contexts, so you can have the precise version — the one interviewers mean when they say the scope chain.
Here it is: every execution context carries, besides its memory table, one outer link — a rope to another context. Which one? The context of the place the function was written (that's what "lexical scoping" means — decided by text position, fixed forever, regardless of who calls it).
Name lookup is a rope-walk: check the current table, miss, follow the rope, repeat — until found, or the global table misses too and you get a ReferenceError.
And with this one picture, closures stop being magic for good.
const app = "notebook";
function outer() {
const page = 3;
function inner() {
const line = 7;
console.log(line + " " + page + " " + app);
}
inner();
}
outer();By the time line 8 runs, three execution contexts exist: global (holding app and outer), outer’s (holding page and inner), and inner’s (holding line). Each holds its own names, in its own memory table.
The deeper story, with the real names for things — this part is what turns “I saw it” into “I can explain it.”
"Lexical" (or "static") scoping means the ropes are tied by where code is written, not where it's called from. Call inner from anywhere — pass it around, store it in an array, invoke it in another function — its rope still points at outer's context.
This is exactly the property this does NOT have (next lesson's drama: this is decided at call time).
The precise closure story: a returned inner function keeps a reference to its outer context. So the garbage collector (lesson 5.7) cannot sweep that context. Its variables stay alive, reachable only through the rope.
One maker call = one kept context, which is why two counters from the same factory never share a count (3.7's independent counters, explained).
Blocks count too: every { } with a let/const inside gets its own small environment on the chain. That is the machinery behind 3.5's block scope. It explains 5.2's note that var ignores blocks — var registers on the nearest function context, sailing straight past block environments.
⌨️ a label maker with a long rope
Build a function factory whose product reaches through TWO ropes: its own context, its maker’s context, and the global context — all in one returned string.
requirements:
- A global
const brand = "jfb". - A function
makeLabel()that declaresconst sep = " · "and RETURNS an inner function. The inner function takes one parameteridand returnsbrand + sep + id— one name from each context on the chain. - Call the factory once, store the product as
label, and printlabel("cart"). (Note that makeLabel has RETURNED by then — and the rope still holds. That’s 3.7’s closure, now with its precise machinery.)
when you press RUN, the console must show exactly:
✏️ Quick check 1
Type exactly what this prints:
const size = "L";
function shop() {
function tag() {
console.log(size);
}
tag();
}
shop();✏️ Quick check 2
This code throws when the last line runs. Type the ERROR NAME:
function bake() {
const temp = 200;
}
bake();
console.log(temp);✏️ Quick check 3
The rope (outer link) is decided by where a function is ___ — type: written or called.
🗣️ Now teach it back
Explain the scope chain precisely to a friend: what each execution context carries, how a name lookup actually proceeds, what finally causes a ReferenceError — and use it to explain why closures work.
Write it as if your friend is sitting next to you. Saved to your journal — future-you will use these notes to teach others.