Labs 04 and 08 both leaned on a "scope-chain search" without explaining it. This lab finally
opens that up: how does JavaScript actually decide which variable a name refers to?
What this lab produces: no files, no commit — a pure concept lab. You'll run
one short snippet in a console or with node.
Concept type: Common Concept, JavaScript-Specific Rules
◎
NAMES & SCOPE2 of 10
Every language needs rules for resolving a name to a value. This lab is JavaScript's answer,
in full.
Mental Models
Mental Model 1 of 5Search Starts Inside, Walks Outward
Looking up a name always starts at the innermost scope surrounding that code, then checks each enclosing scope one level out, until it's found or there's nowhere left to check.
Mental Model 2 of 5Lexical = Fixed by Where It's Written
A function's scope chain is locked in based on where it was defined in the source code — calling it from somewhere completely different never changes which outer variables it can see.
Mental Model 3 of 5First Match Wins (Shadowing)
If two scopes on the chain both have a variable with the same name, the search stops at the first (innermost) one it finds — the outer one is never even reached, let alone used.
Mental Model 4 of 5An Analogy: Family, Then Neighbors, Then Town
If you need to know something, you'd ask your immediate family first, then your neighbors if nobody at home knew, then the wider town only as a last resort — you never skip straight to asking a stranger across town before checking with the people closest to you.
Mental Model 5 of 5Misconception: Scope Chain ≠ Call Stack
The call stack (Lab 05) tracks who called whom, changing with every call and return. The scope chain tracks where code was written, fixed the moment a function is defined. A function can be called from a completely different place in the call stack than where it was written in the scope chain — the two are entirely independent structures.
Learning Objectives
Objective 1 of 4 — I can describe how JavaScript resolves a variable name by walking outward through nested lexical environments.
Objective 2 of 4 — I can predict which value a shadowed (same-named) variable resolves to, given a specific nesting structure.
Objective 3 of 4 — I can explain why "lexical" scope depends on where code is written, not on which function called it.
Objective 4 of 4 — I can explain why the scope chain and the call stack are two genuinely different structures, even though they can look similar in simple examples.
Pre-Lab Quiz
Answer from intuition first.
Question 1 of 5A variable named count exists both in the global scope and inside a function. Inside that function, does referencing count find the global one or the function's own one?Open question
Question 2 of 5A function is defined inside another function, then handed out and called from a completely unrelated place. Does its scope chain follow where it was called from, or where it was written?Open question
Question 3 of 5If a name isn't found anywhere in the scope chain, all the way up to global, what happens?Open question
Question 4 of 5Two variables share the same name at two different nesting levels. Does the search find the outer one or the inner one first?Open question
Question 5 of 5Is a function's scope chain rebuilt fresh every time it's called, or fixed once when it's defined?Open question
The Concept
Name resolution isn't a JavaScript invention — lexical scoping specifically is common too
Every language needs rules for what a name refers to. JavaScript's approach —
lexical scoping — is actually one of the more common choices, shared with
many mainstream languages, including Python.
Cross-language note
Common concept: resolving a name by searching outward through enclosing scopes.
Python: uses the same basic idea, often summarized as LEGB — Local, then
Enclosing function, then Global, then Built-in — searched in that order.
JavaScript: the same walk-outward idea, just built from a chain of
lexical environments instead of Python's named layers — the underlying search strategy is
genuinely similar.
A lexical environment is not the same thing as an execution context's caller link
Lab 05 described an execution context as holding, among other things, "a link back to
whoever called it." That caller link exists purely so the engine knows where to resume once
a function returns — it has nothing to do with variable lookup. A lexical
environment is a separate concept: a record of a scope's own variables, plus a
link to the lexically enclosing scope — the scope of whatever code physically
contains this one in the source. Confusing these two different links is exactly what
produces the scope-chain-vs-call-stack misconception from the fifth mental model above.
The search, step by step
Start at the lexical environment for the code currently running.
Check whether that environment has an own variable with the name being looked up.
If yes, stop — that's the value, regardless of what might exist further out.
If no, move to the environment one level out (the lexically enclosing scope) and repeat.
If the walk reaches the global environment and still finds nothing, the lookup fails with a ReferenceError (Lab 04).
This chain of "own variables, plus a link to the next environment out" is what people mean
by the scope chain — and because each link is set once, at the moment a
function is defined, the whole chain is lexical: fixed by where code sits in the
source file, never by how or from where it's later called.
Lab
Primary objective: prove the scope chain is lexical (fixed at definition) rather than dynamic (dependent on the caller), directly.
You'll need: a browser console or node — no project, no files.
Lab 1 of 1 — Prove scope is lexical, not dynamic
Task
Define a function inside one scope, hand it out, and call it from a scope with a same-named variable — see which one it actually uses.
Instructions
Step 1 of 4
// Paste this into a scratch file or console. Read it — do not run it yet.
var teamName = "Falcons";
function announceTeam() {
console.log("Now playing:", teamName);
}
function runInAnotherContext() {
var teamName = "Wolves";
announceTeam();
}
runInAnotherContext();
This step is complete when: you've predicted which team name prints, without running it.
Step 2 of 4
Predict which team name announceTeam() logs — the one from where it was written, or the one from where it was called.
Output: Now playing: Falcons
What you wrote: announceTeam is defined at the top level, where
teamName is "Falcons". It's then called from inside
runInAnotherContext, which has its own separate teamName set to
"Wolves".
What the engine effectively does: announceTeam's scope chain was
fixed the moment it was defined -- top-level scope, where teamName
is "Falcons". Being CALLED from inside a function with a different
teamName changes nothing about that chain. The "Wolves" variable
is never even reachable from inside announceTeam's own lexical
environment.
This step is complete when: you've run the snippet yourself and it matches the revealed output.
Step 3 of 4
// Edit the file yourself. Don't paste finished code — type it from these comments.
// Move the entire "function announceTeam() {...}" declaration
// so it's now defined INSIDE runInAnotherContext, placed after
// "var teamName = 'Wolves';" and before the call to announceTeam().
// Leave the top-level "var teamName = 'Falcons';" and the final
// runInAnotherContext(); call exactly where they are.
This step is complete when:announceTeam is now defined inside runInAnotherContext, after its teamName declaration.
Step 4 of 4
Predict: does announceTeam() log a different team name now that it's defined in a different place? Run it and compare, then answer this comprehension check: what specifically changed between the two versions — where the function was called from, or where it was written?
Comprehension Checklist
I can trace, step by step, how a name lookup walks outward through nested scopes.
I can predict which of two same-named variables a shadowed reference resolves to.
I can explain why moving a function's call site doesn't change its scope chain, but moving its definition does.
I know the scope chain (fixed at definition) and the call stack (changes with every call) are two separate structures.
Post-Lab Quiz
These are different questions from the pre-lab quiz — they test what the experiment should
have taught you, not the same questions again.
Question 1 of 5In the original version of the experiment, why did announceTeam() log "Falcons" and not "Wolves"?Open question
Question 2 of 5After moving announceTeam's definition inside runInAnotherContext, what changed about its output?Open question
Question 3 of 5Based on both versions of the experiment, which single factor actually determines a function's scope chain?Open question
Question 4 of 5If a third, unrelated function also named teamName "Eagles" existed somewhere completely separate in the file, would it ever affect announceTeam's output in either version of the experiment?Open question
Question 5 of 5Is the scope-chain lookup process this lab demonstrated part of the call stack mechanism from Lab 05, or a separate mechanism running alongside it?Open question
Reflection Questions
Write your own answer first — the blurred text is one possible answer, not the only correct one.
Reflection 1 of 3. Before running Step 1, what did you predict — "Falcons" or "Wolves"? If you predicted "Wolves," what assumption were you making about how scope works?
Predicting "Wolves" usually means assuming scope follows the call stack — that a
function somehow "inherits" whatever variables exist wherever it happens to be called
from. That's how some other approaches to scoping actually do work (dynamic scoping),
just not how JavaScript's lexical scoping works.
Reflection 2 of 3. Lab 05 mentioned an execution context holds "a link back to whoever called it." Now that you've done this lab, explain in your own words why that caller link and the scope-chain link are not the same thing.
The caller link exists purely to know where to resume execution once a function returns
— it changes every time the function is called from somewhere new. The scope-chain link
exists purely for name lookup, and is fixed once at the moment the function is defined,
regardless of how many different places later call it.
Reflection 3 of 3. Explain to an imaginary coworker, in one or two sentences, why "lexical" is the right word for how JavaScript's scoping works.
Something like: "lexical" refers to the text of the source code itself — and JavaScript's
scope rules are decided entirely by looking at that text (where a function is physically
written), never by anything that happens dynamically while the program runs.
What Breaks If This Understanding Is Wrong?
Closures (Lab 14) are built entirely on the scope chain staying fixed at a function's
definition, even long after the code that defined it has finished running. If you think scope
follows the call stack instead, closures will look like unpredictable magic instead of a
direct, logical consequence of what this lab just demonstrated.
What JavaScript Concept Was Learned Today?
The scope chain: a name lookup walks outward through lexical environments, from innermost
to outermost, fixed entirely by where code is written — never by where it's called from.
Relates to: ◎ Names & Scope (2 of 10) — this experiment demonstrated
JavaScript's specific answer to "how does the language decide which variable a name refers to."
Next
Lab 10 takes a short but important detour to make one assumption explicit: JavaScript runs
one thing at a time, on one thread, always. That constraint — which every lab from Lab 05
onward has quietly relied on — becomes essential once functions and closures start getting
passed around and called later.