Phase 2 · Core JavaScriptModule 9~40 min read

Scope, Hoisting & the Temporal Dead Zone

Understand where bindings live, when they become usable, and why modern declarations prevent entire classes of bugs.

What you'll learn

Scope determines where a name can be used; hoisting determines when its declaration is registered. A precise model prevents mysterious ReferenceErrors and accidental shared state.

By the end of this lesson, you'll be able to:

  • Trace a name through lexical scopes
  • Predict hoisting and temporal-dead-zone behavior
  • Choose declarations that keep state local

Core mental model

Use this decision table as a compact reference. Focus on what each tool means and when it earns its place in production code.

ConceptWhat it meansDecision rule
Lexical scopeVisibility follows source-code nestingDeclare data in the narrowest block that owns it
HoistingDeclarations are registered before executionDo not rely on use-before-declaration
TDZlet/const exist but cannot be read before initializationDeclare bindings close to first use

Professional workflow

Build the behavior in small, observable steps. Each step should leave something you can inspect or test.

  1. Describe the binding and scope boundary: inputs, outputs, state, timing, and expected failures.
  2. Implement the smallest correct path with names that expose intent.
  3. Add edge cases and failure handling before introducing abstractions.
  4. Verify behavior with realistic data and one deliberately adversarial example.
  5. Refactor only after the observable behavior is protected.

Make behavior observable

Before optimizing or abstracting, make inputs, outputs, state changes, timing, and failure paths visible. JavaScript becomes much easier to reason about when hidden work is exposed.

Guided code lab

Follow the scope chain

The inner function reads its own parameter and then closes over the nearest matching outer binding.

scope.js
const label = "global";
function makeReporter(label) {
  const prefix = "Course";
  return function report() {
    console.log(prefix + ": " + label);
  };
}
makeReporter("JavaScript")();

Block scope prevents leakage

A binding created inside a branch belongs only to that block.

block-scope.js
function priceLabel(price) {
  if (price < 0) {
    const message = "Invalid price";
    return message;
  }
  const message = "$" + price.toFixed(2);
  return message;
}
console.log(priceLabel(12));

Production practice

Contract

A binding should be visible only where its invariant is understood.

Verification

Mark each declaration and predict every lookup before running the program.

Operations

Treat unexpected globals and shadowed names as defects during review.

Common failure mode

Using var or assigning an undeclared name can leak state beyond the block that appears to own it.

Independent workshop

Refactor a script with global counters and shadowed names into small functions with block-scoped state.

Your finished workshop must include:

  • No var declarations or implicit globals
  • A written scope trace for one nested lookup
  • Tests for a valid and invalid input

Definition of done

Demonstrate the happy path and at least two edge cases, keep responsibilities separated, and add a short note explaining one design choice.

Recap & quick check

Key takeaways

  • Scope is lexical
  • let and const are block scoped
  • The TDZ makes premature access an error
  • Narrow visibility reduces coupling

Quick check

1. What decides lexical scope?

2. Can const be read before its declaration line?

3. Which declaration is function-scoped?

Keep the workshop. Later modules deliberately build on these decisions, so each exercise can become part of your final portfolio architecture.