Scope and hoisting
Predict identifier resolution, declaration initialization, block scope, function scope, shadowing, and the temporal dead zone.

WHAT YOU WILL BE ABLE TO DO
Learning outcomes
- Explain scope and hoisting in plain language.
- Connect the behavior to the underlying browser or framework model.
- Implement the core pattern and reason through edge cases.
- Answer common follow-ups without relying on memorized phrases.
01Overview
Scope is determined lexically — by where code is written, not where it's called from. Hoisting is the engine's creation-phase step of registering declarations before execution begins.
02Mental model
var is function-scoped and hoisted with an undefined initial value. let and const are block-scoped and hoisted into a temporal dead zone (TDZ): the binding exists but accessing it before its declaration line throws a ReferenceError.
03Examples
{ console.log(x); // ReferenceError (TDZ) let x = 1;}if (true) { var y = 2;}console.log(y); // 2 — var leaks out of the block04Check understanding
Why doesn't let leak out of an if block the way var does?
let is scoped to the nearest enclosing block, not the nearest function, so its binding ends at the closing brace.
DDConcept deep dives
Deep dive 1
Scope determines where a name resolves
Identifier resolution starts with the current lexical environment and follows outer environment links until a matching binding is found or resolution fails. JavaScript has global, module, function, block, catch, and other environment forms. Scope is lexical because source structure determines those links; the place from which a function is called does not change its outer lexical scope.
- Inner declarations can shadow outer names without modifying the outer binding.
- ES modules give every file a private top-level scope.
- Dynamic features such as with and direct eval complicate optimization and reasoning.
Deep dive 2
Hoisting is several different initialization rules
Saying declarations move to the top hides important differences. During scope instantiation, var is created and initialized to undefined; a function declaration is initialized with its function object; let, const, and class bindings are created but uninitialized. The temporal dead zone is simply the region where code can resolve a lexical name but cannot read its uninitialized binding.
- typeof is not safe for a lexical binding still in its temporal dead zone.
- A const binding must be initialized and cannot be reassigned, but its object can still mutate.
- Duplicate var declarations and duplicate lexical declarations follow different early-error rules.
Deep dive 3
Loop bindings explain callback behavior
A for loop declared with let creates a distinct binding for each iteration, so callbacks naturally observe the iteration that created them. A var loop has one function-scoped binding shared by every callback. This is not a special timer rule; it follows directly from which environment and binding each closure references.
- Prefer let for a changing loop index and const for per-iteration values that do not reassign.
- A factory or IIFE creates an environment explicitly when maintaining older code.
- Using an array index as UI identity is a separate problem from closure capture.
QAInterview questions and model answers
Attempt each answer aloud before opening it. The model answer shows the depth and precision expected in an interview; it is not a script to memorize.
Intermediate · Conceptual · 1 min · Question 1What does hoisting actually mean?Open model answer
Model answer
Hoisting is informal shorthand for declaration instantiation before statement evaluation. Different declarations are initialized differently: function declarations receive their function value, var receives undefined, while let, const, and class remain uninitialized until their declaration is evaluated.
Open question page →Intermediate · Coding · 1 min · Question 2What is the temporal dead zone?Open model answer
Model answer
It is the period from entering a lexical scope until a let, const, or class declaration is initialized. Access during that period throws a ReferenceError, including typeof on the uninitialized binding. This catches use-before-initialization bugs.
{ // TDZ for `x` starts here console.log(typeof x); // ReferenceError (not "undefined") let x = 1; // TDZ ends here}Intermediate · Coding · 1 min · Question 3How do function scope and block scope differ?Open model answer
Model answer
var and function declarations in ordinary functions are function-scoped, subject to additional block-function rules. let, const, and class are scoped to the nearest block. A for loop with let creates per-iteration bindings used by closures.
if (true) { var a = 1; let b = 2;}console.log(a); // 1 — var ignores the blockconsole.log(b); // ReferenceError — let is block-scopedIntermediate · Conceptual · 1 min · Question 4What is shadowing?Open model answer
Model answer
An inner declaration can use the same name as an outer binding, causing resolution inside the inner scope to select the nearer binding. It is legal in many cases but can reduce clarity; some var/lexical combinations are early syntax errors.
Open question page →Intermediate · Conceptual · 1 min · Question 5Does var attach to globalThis?Open model answer
Model answer
At the top level of a classic browser script, a global var declaration generally creates a property on the global object. Top-level let and const do not. ES modules have their own module scope and do not expose top-level declarations as global object properties.
Open question page →Beginner · Coding · 1 min · Question 6Why prefer const by default?Open model answer
Model answer
const prevents reassignment of the binding, communicates intent, and narrows possible state changes. It does not freeze the referenced object. Use let when the binding itself must change and avoid var in modern application code unless its semantics are specifically required.
const user = { name: 'Ada' };user.name = 'Grace'; // OK — the object is mutableuser = {}; // TypeError — the binding cannot be reassignedIntermediate · Conceptual · 1 min · Question 7Are function expressions hoisted like function declarations?Open model answer
Model answer
The variable declaration follows its own rules, but assignment of the function expression occurs during evaluation. A var-bound expression is therefore undefined before the assignment.
Open question page →Beginner · Conceptual · 1 min · Question 8What scope do catch parameters have?Open model answer
Model answer
A catch parameter has a binding scoped to the catch block. Modern optional catch binding can omit it when the thrown value is not needed.
Open question page →Advanced · Conceptual · 1 min · Question 9Can let and var declare the same name in one scope?Open model answer
Model answer
Conflicting lexical and var declarations can produce an early SyntaxError. Exact behavior depends on their containing scopes, so nested blocks must be analyzed precisely.
Open question page →Advanced · Conceptual · 1 min · Question 10How does direct eval affect scope?Open model answer
Model answer
Direct eval can interact with the current execution context according to strictness and declaration rules, making static reasoning and optimization harder. Application code should avoid it.
Open question page →SCScenario questions
Scenario 1
Explain why accessing a let variable before its declaration throws instead of returning undefined like var.
- Describe scope creation before execution.
- Distinguish creation from initialization.
- Explain var's initialization to undefined.
- Explain the lexical binding's uninitialized state and TDZ.
Reveal worked answer
Both bindings are known when the scope is instantiated. The difference is initialization: var is initialized to undefined immediately, while let remains uninitialized until evaluation reaches the declaration. Reading an uninitialized lexical binding throws, preventing code from silently using a value before initialization.