Skip to content
Beginner10 min study

Scope and hoisting

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

Question progress0 / 10 completed
Start the lesson
Scope and hoisting visual explanation

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.

Think of it like this: var is a shout that echoes through the whole building (function scope) — everyone on every floor can hear it — while let and const are a quiet conversation that only the people in the same room (block) can hear.

03Examples

JavaScript
{  console.log(x); // ReferenceError (TDZ)  let x = 1;}if (true) {  var y = 2;}console.log(y); // 2 — var leaks out of the block

04Check 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.

How to say it out loud: "Scope is about where in the code a variable is visible, and it's determined by where the code is written, not where it's called from. var is function-scoped and gets hoisted with an initial value of undefined, which is why reading a var before its declaration line gives you undefined instead of an error. let and const are block-scoped, and they're also hoisted, but into what's called the temporal dead zone — the binding technically exists, but trying to read it before the declaration line actually runs throws a ReferenceError. It's not that let isn't hoisted, it's that hoisting a var initializes it immediately, while hoisting let or const doesn't."

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.

JavaScript
{  // TDZ for `x` starts here  console.log(typeof x); // ReferenceError (not "undefined")  let x = 1;             // TDZ ends here}
Open question page →
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.

JavaScript
if (true) {  var a = 1;  let b = 2;}console.log(a); // 1 — var ignores the blockconsole.log(b); // ReferenceError — let is block-scoped
Open question page →
Intermediate · 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.

JavaScript
const user = { name: 'Ada' };user.name = 'Grace'; // OK — the object is mutableuser = {};           // TypeError — the binding cannot be reassigned
Open question page →
Intermediate · 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.

  1. Describe scope creation before execution.
  2. Distinguish creation from initialization.
  3. Explain var's initialization to undefined.
  4. 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.

Verify and go deeper