Lab 05 showed a context gets pushed and popped. Now we open one up: it's actually built in
two separate passes, and that's what makes "hoisting" look the way it does.
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: Mostly JavaScript-Specific
▶
EXECUTION10 of 10
◎
NAMES & SCOPE2 of 10
Hoisting sits at the intersection of how code executes and where names become visible.
Mental Models
Mental Model 1 of 5Two Passes, Same Code
Nothing runs during the first pass — it only registers what exists. The second pass is the one that actually executes line by line.
Mental Model 2 of 5Registered Empty vs Registered Full
Both get a slot in pass 1, but a var starts empty while a function declaration starts already holding the whole function.
Mental Model 3 of 5Hoisting Is Not Teleportation
The lines of your file never move. Only the registration step happens before execution reaches them — it just looks like the declaration "moved up."
Mental Model 4 of 5An Analogy: Setup Before Opening
Before a restaurant opens its doors, the staff are already hired and the menu is already printed — that's the setup phase, done with the doors still closed. Only once setup is complete does the "open" sign go up and customers actually get served, in order, one at a time. The creation phase is the closed-doors setup; the execution phase is the open sign going up.
Mental Model 5 of 5Misconception: Hoisting Isn't a Feature to Use
Knowing hoisting exists is essential for reading other people's code correctly. It is not a recommendation to deliberately write code that depends on calling things before they "appear" — that habit breaks the moment a function declaration becomes an expression, an arrow function, or moves into a conditional block (Lab 07 shows exactly how differently let/const behave here).
Learning Objectives
Objective 1 of 4 — I can describe the creation phase and execution phase as two distinct passes over the same code, not one single pass.
Objective 2 of 4 — I can predict what a var-declared variable holds, and what calling a function declaration returns, when either is used before its own line in the file runs.
Objective 3 of 4 — I can explain why converting a function declaration into a function expression changes what happens when it's called early.
Objective 4 of 4 — I can explain "hoisting" accurately, without describing it as code physically moving.
Pre-Lab Quiz
Answer from intuition first.
Question 1 of 5You log a var-declared variable on the line before its declaration and assignment. What happens?Open question
Question 2 of 5You call a function declaration on the line before its definition appears in the file. What happens?Open question
Question 3 of 5Does JavaScript actually reposition your declarations to the top of the file before running it?Open question
Question 4 of 5Two function declarations, A and B, each call each other, and B is written above A in the file even though A is called first. Does this work?Open question
Question 5 of 5var x = 1; appears twice in the same scope (var x = 1; ... var x = 2;). Does the second one cause an error?Open question
The Concept
Building an execution context is a two-step process
When the engine builds an execution context (Lab 05), it doesn't immediately start running
your code top to bottom. It first does a full pass over that context called the
creation phase, registering every var and function declaration
it finds — before a single line has actually executed. Only after that's done does the
execution phase begin, running your code top to bottom for real.
Why register everything up front, in a separate pass?
Consider two functions that need to call each other — function A calls function B
somewhere in its body, and function B calls function A somewhere in its. If the engine only
recognized a function's existence once execution physically reached its definition line,
this kind of mutual reference would be nearly impossible to write, since whichever one is
defined second wouldn't exist yet when the first one's definition is being read. Registering
every function declaration's full value up front, before any code runs, sidesteps that
problem entirely — by the time any code executes, every function declaration in that scope
already exists, regardless of the order they were written in.
function isEven(n) {
if (n === 0) return true;
return isOdd(n - 1); // isOdd is used here...
}
function isOdd(n) { // ...but only defined below
if (n === 0) return false;
return isEven(n - 1);
}
What gets registered, and how
Every var name is registered and set to undefined — a slot
exists, but empty.
Every function declaration is registered with its entire function
already as the value — fully ready to call.
The execution phase then runs top to bottom, assigning real values to vars
as their lines are reached.
This two-phase process is what people call "hoisting" — but nothing physically moves, as
the third mental-model diagram above shows. The registration was always going to happen
first; it only looks like the declaration relocated.
Lab
Primary objective: observe the split between "registered" and "assigned" directly, for both a var and a function declaration.
You'll need: a browser console or node — no project, no files.
Lab 1 of 1 — Watch the two-phase split happen
Task
Use each name before its own line in the file runs, and see which one is "empty but safe" versus "fully ready."
Instructions
Step 1 of 4
// Paste this into a scratch file or console. Read it — do not run it yet.
console.log(nickname);
announce();
var nickname = "Milo";
function announce() {
console.log("hi, I'm " + nickname);
}
This step is complete when: you've predicted what each of the first two lines actually does when it runs.
Step 2 of 4
Predict what console.log(nickname) prints, and what calling announce() does at that point in the file.
console.log(nickname) -> undefined
announce() -> logs "hi, I'm undefined"
What you wrote: code that reads nickname and calls announce()
before either "appears" in the file.
What the engine effectively does: during the creation phase it
already registered nickname (as undefined) and announce (as the
complete function). By the time line 1 runs, both names already
exist -- nickname just hasn't been assigned "Milo" yet, because
that assignment is execution-phase code that hasn't run yet. When
announce() runs, it reads whatever nickname currently holds at
that moment -- still undefined.
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.
// Change the function declaration to a function expression:
// replace "function announce() {...}" with
// "var announce = function () {...}", keeping the same body.
// Leave the two calls at the top of the file exactly where they are.
This step is complete when:announce is now a var holding a function expression, not a function declaration.
Step 4 of 4
Predict: does announce() still work at the top of the file now, or does something change? Run it and compare, then answer this comprehension check: in your own words, why does a function declaration survive being called early, while a function expression assigned to a var does not?
Comprehension Checklist
I can describe the creation phase and execution phase as two separate passes, not one.
I know a var registers as undefined, while a function declaration registers with its full value.
I know "hoisting" doesn't move code — it's just that registration happens before execution.
I can explain why two mutually-calling function declarations work regardless of which one is written first.
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 5console.log(typeof mystery); var mystery = 5; — what does this log? (Tie this back to Lab 03's typeof.)Open question
Question 2 of 5Why did converting announce from a function declaration to a var-assigned function expression change the outcome?Open question
Question 3 of 5Does JavaScript literally move declarations to the top of the file at runtime?Open question
Question 4 of 5Would the isEven/isOdd mutual-recursion example from the Concept section work if isOdd were rewritten as a var-assigned function expression instead of a declaration?Open question
Question 5 of 5Which happens first: the creation phase finishing for the whole file's top-level code, or the very first line of that code actually executing?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. Even though calling a function declaration before its line "works," why might relying on that be a risky habit?
It works for a plain function declaration today, but the exact same-looking code breaks
silently (or throws) the moment someone refactors it into a function expression, an
arrow function, or moves it into a conditional block. Reading code top to bottom, in the
order it actually runs, avoids depending on a subtlety that's easy to break by accident.
Reflection 2 of 3. How does the two-phase model explain why declaring the same var name twice in one scope doesn't cause an error?
The creation phase only registers a name once per scope, regardless of how many times
var mentions it — so a second var statement for the same name
isn't creating a new binding, it's just another assignment to the one that already exists.
Reflection 3 of 3. Before this lab, would you have predicted that the mutual-recursion example (isEven/isOdd) could work at all, given that each function references the other before it's "been defined" in file order? What does that reveal about how you were thinking about "defined"?
Many learners assume "defined" means "the engine has reached that line while running" —
a purely execution-phase idea. This lab shows "defined" for a function declaration
actually happens earlier, during creation, which is exactly what makes mutual references
between two function declarations possible at all.
What Breaks If This Understanding Is Wrong?
Without the two-phase model, the Temporal Dead Zone (Lab 07) looks like an arbitrary rule
instead of a direct consequence of it — let/const go through the
same creation phase, just registered in a different, inaccessible state.
What JavaScript Concept Was Learned Today?
Every execution context is built in two passes — a creation phase that registers names
first, then an execution phase that actually runs the code — and that's the real mechanism
behind what's casually called "hoisting."
Relates to: ▶ Execution (10 of 10) + ◎ Names & Scope (2 of 10) —
hoisting is what happens when "how code runs" and "when a name becomes visible" meet.
Next
Lab 07 asks what happens when let and const go through this same
creation phase instead of var — and shows they land in a much stricter,
locked state called the Temporal Dead Zone, directly explained by the two-phase model you
just built.