Chapter 12 of 28 · JavaScript for Beginners — Complete Free Tutorial

Scope, Hoisting & Closures

“Why is my variable undefined?” and “why does this handler remember the wrong value?” are scope questions. This chapter explains where variables exist (scope), what happens before lines run (hoisting), and how functions remember their birthplace (closures) — three ideas that separate beginners from confident intermediate developers.

Three kinds of scope

ScopeCreated byVisible to
GlobalTop level of a scriptEverything in the page (avoid — pollutes)
FunctionInside a function (var and parameters)That function and its nested functions
BlockInside { } (let / const)That block only
const outer = 'visible';        // module/global scope

function demo() {
  const inner = 'function scope';
  if (true) {
    const blocked = 'block scope';
    console.log(outer, inner, blocked);   // all three ✅
  }
  console.log(blocked);          // ❌ ReferenceError
}
console.log(inner);              // ❌ ReferenceError

Scope chain: reading outward

Inner scopes can read outer variables (the scope chain), but never the reverse:

let a = 1;
function outerFn() {
  let b = 2;
  function innerFn() {
    let c = 3;
    console.log(a, b, c);   // → 1 2 3  (inner sees all the way out)
  }
  innerFn();
  console.log(c);           // ❌ inner's locals are gone from here
}
outerFn();

Lookup runs inward-out: local → enclosing → global. If a name is not found anywhere, JavaScript throws ReferenceError (unless it is a property access, which yields undefined).

Shadowing

let x = 'global';
function shadow() {
  let x = 'local';        // shadows the outer x
  console.log(x);         // → 'local'
}
shadow();
console.log(x);           // → 'global'

Hoisting: declarations move up

The engine creates declarations before executing, but the initialisation timing differs by keyword:

console.log(greet());        // → 'hi' (function declarations hoist whole)

console.log(name);           // ❌ ReferenceError (let/const in the temporal dead zone)
var legacy = 'available as undefined before this line';
console.log(legacy);         // → 'available…'

function greet() { return 'hi'; }
KeywordHoisted?Before its line runs…
function declarationFullyAlready callable
varYesExists as undefined
let / constBinding onlyTemporal Dead Zone — any access throws

The TDZ is a feature: it catches use-before-declaration bugs that var hides. Practical rule: declare near first use, at the top of the scope.

Closures: functions that remember

A closure is a function bundled with the variables from the place it was created. Those variables stay alive as long as the function does:

function makeCounter() {
  let count = 0;                    // private variable
  return function () {
    count++;                        // still reachable!
    return count;
  };
}

const c1 = makeCounter();
console.log(c1());   // → 1
console.log(c1());   // → 2
const c2 = makeCounter();
console.log(c2());   // → 1  (separate closure, separate memory)

Expected output: each counter has its own count — invisible from outside, safe from accidental changes. That is encapsulation without classes.

Closures in everyday code

// 1. Loop-safe event data (classic use)
document.querySelectorAll('.btn').forEach((btn, i) => {
  btn.addEventListener('click', () => console.log(`Pressed #${i}`)); // each handler keeps its own i
});

// 2. Debounce: remember the timer between calls
function debounce(fn, delay) {
  let timer;
  return (...args) => {
    clearTimeout(timer);
    timer = setTimeout(() => fn(...args), delay);
  };
}

// 3. Data privacy in plain objects
function createUser(name) {
  return {
    getName: () => name,       // readable only through the closure
  };
}

The classic loop gotcha

for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i));   // → 3 3 3 (one shared var)
}
for (let i = 0; i < 3; i++) {
  setTimeout(() => console.log(i));   // → 0 1 2 (let: fresh binding per pass)

let creates a new binding each iteration, so every callback closes over its own i. This is the number-one reason modern code never uses var.

Closures and memory

Every closure keeps its captured variables alive. That is usually trivial; issues appear only when a closure retains huge objects (an entire component/DOM tree) inside a long-lived listener. Remove listeners you no longer need, and avoid closures inside tight loops over large data.

Common mistakes

  • Reading a let before declaration — TDZ ReferenceError; declare first.
  • Expecting block scope from var — it leaks from every block.
  • Accidental globals — assigning without declaring (total = 5) creates a window property in sloppy mode; modules (chapter 23) prevent this entirely.
  • Shared-object closures in loops — capture what you need in the function's own parameters (((i) => () => i)(i)) or use let.
  • Confusing closure with scope — scope is visibility now; a closure is visibility that persists after creation.

Practice checklist

  1. Build a makeBankAccount(initial) factory with deposit/balance where balance is inaccessible directly.
  2. Predict the output of nested functions printing outer variables — then run it.
  3. Reproduce the var loop bug, then fix it with let.
  4. Write a once(fn) helper that lets a function run only one time (closure + flag).

Now that you know how values are remembered — one more foundational puzzle remains before we touch the page: what does this mean, and why does it change? Next: The this Keyword & Binding.

Quiz: Scope, Hoisting & Closures

5 questions · 70% to pass

Frequently Asked Questions

JavaScript is the programming language of the web. It adds behaviour to pages: responding to clicks and input, validating forms, fetching data from APIs, updating the DOM without reloading, and much more. It also runs on servers (Node.js), in desktop and mobile apps, and in build tooling.

They are unrelated languages that share a similar name for historical marketing reasons. Java is a statically-typed compiled language; JavaScript is a dynamically-typed interpreted language designed for the browser. Knowing one does not imply knowledge of the other.

Helpful but not mandatory — HTML gives you the structure JavaScript manipulates and CSS the styling it toggles. If you know basic tags, classes and selectors, you have enough to start; the DOM chapters assume only that much.

Use const by default and let when the value must change. Avoid var entirely in modern code: it is function-scoped instead of block-scoped, hoists differently, and can create accidental globals. This course uses const and let throughout.

The Document Object Model is the browser’s in-memory tree of objects representing your HTML. JavaScript reads and modifies that tree — selecting elements, changing text, adding nodes, attaching event listeners — and the browser re-renders the page to match.

== compares after converting types (so 5 == "5" is true), while === requires the same value AND the same type (5 === "5" is false). Loose equality has famously confusing rules — use === and !== everywhere unless you have a specific reason not to.

JavaScript is single-threaded, so long-running work (timers, fetches, events) is handled by the browser and its callbacks are queued. The event loop moves queued callbacks onto the call stack only when it is empty, with promise callbacks (microtasks) taking priority over timers. It is why asynchronous code never freezes the page.

A Promise is an object representing a future value — pending, fulfilled or rejected — that can be chained and combined (Promise.all). async/await is syntax sugar over promises that lets asynchronous code read top-to-bottom with ordinary try/catch error handling.

Yes. Frameworks are built on exactly these fundamentals — components are functions, state is your variable discipline, effects are events, rendering is DOM work. Developers who learn vanilla first understand what the framework is doing and debug far faster.

Type out the examples instead of copying, build small end-to-end projects (expense tracker, weather app, quiz), handle every UI state (loading, empty, success, error), and revisit fundamentals while reading real code. Consistency beats volume — a little most days outpaces occasional marathons.