← all labs  ·  ← prev lab  ·  next lab →

Foundation Lab 09

Lexical Environment & the Scope Chain

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 & SCOPE 2 of 10

Every language needs rules for resolving a name to a value. This lab is JavaScript's answer, in full.

Mental Models

Nested scopes, search walks outward Three nested rectangles, innermost highlighted, with an arrow walking outward from inner to outer. start here

Mental Model 1 of 5Search Starts Inside, Walks Outward

Lexical means fixed by where code is written, not where it's called from A function defined inside a box labeled written here, called from a completely different box labeled called from here, with the scope link pointing back to written here. written here called from here

Mental Model 2 of 5Lexical = Fixed by Where It's Written

First match wins, search stops there Two boxes both containing a variable named count, the inner one highlighted and marked found, the outer one marked never reached. count (outer) — never reached count (inner) — found here

Mental Model 3 of 5First Match Wins (Shadowing)

An analogy: asking family, then neighbors, then the whole town Three concentric circles: immediate family in the center, neighbors in the middle ring, the whole town in the outer ring. whole town neighbors family

Mental Model 4 of 5An Analogy: Family, Then Neighbors, Then Town

Misconception: the scope chain is not the same as the call stack Two separate diagrams side by side: a call stack with three entries in one order, and a scope chain with a different, unrelated nesting. call stack scope chain ≠

Mental Model 5 of 5Misconception: Scope Chain ≠ Call Stack

Learning Objectives

  1. Objective 1 of 4 — I can describe how JavaScript resolves a variable name by walking outward through nested lexical environments.
  2. Objective 2 of 4 — I can predict which value a shadowed (same-named) variable resolves to, given a specific nesting structure.
  3. Objective 3 of 4 — I can explain why "lexical" scope depends on where code is written, not on which function called it.
  4. 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

  1. Start at the lexical environment for the code currently running.
  2. Check whether that environment has an own variable with the name being looked up.
  3. If yes, stop — that's the value, regardless of what might exist further out.
  4. If no, move to the environment one level out (the lexically enclosing scope) and repeat.
  5. 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.

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?

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.

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.


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.

← All labs  ·  Lab 10 — Single-Threaded, Synchronous Execution →