JavaScript › Core JavaScript

Scope, hoisting, and closures

5 min read Intermediate 5 sections

Scope is where a variable can be seen, and it’s one of the things that separates “I can write a line of JavaScript” from “I can read a real codebase.” Closures — functions that remember the variables around them — sound abstract but are everywhere in modern code. Understanding both makes minified bundles far less mysterious.

You'll learn to

  • Predict where a variable is visible
  • Understand why var differs from let and const
  • Read closures and know what state they capture

Block scope vs function scope

function demo() {
  if (true) {
    let blockScoped = "only inside this block";
    var funcScoped = "visible in the whole function";
  }
  // console.log(blockScoped);  // ERROR — let is block-scoped
  console.log(funcScoped);      // works — var leaks out of the block
}

let and const are block-scoped — they exist only inside the nearest { } braces. var is function-scoped — it leaks out of blocks to the whole function. That leakage is exactly why modern code avoids var: it causes variables to be visible where you didn’t expect.

Hoisting — declarations move up

console.log(x);   // undefined (not an error!) — var declaration is "hoisted"
var x = 5;

console.log(y);   // ERROR — let is hoisted but not initialised (temporal dead zone)
let y = 5;

JavaScript “hoists” declarations to the top of their scope before running. With var, the variable exists (as undefined) before its line, which causes confusing bugs. With let/const, using it before its declaration is an error — a deliberately safer design. You don’t need to write hoisting-dependent code; you need to recognise it when reading.

Closures — functions that remember

A closure is a function that keeps access to the variables from where it was created, even after that outer function has finished.

function makeCounter() {
  let count = 0;                 // private to this counter
  return function () {
    count = count + 1;           // the inner function "closes over" count
    return count;
  };
}

const next = makeCounter();
next();   // 1
next();   // 2  — count persisted between calls

The inner function “remembers” count even after makeCounter returned. That captured count is private — nothing outside can touch it directly. This is how JavaScript creates private state, and it’s everywhere: module patterns, event handlers, React hooks.

Checkpoint

Why does the counter returned by makeCounter keep incrementing across calls, instead of resetting to 1 each time?

Try it yourself

In the console, write a function that takes a secret string and returns a function that, when called, returns that secret. Call the outer function once, store the result, then call the inner function — notice it still knows the secret even though the outer function has finished. That’s a closure capturing state.

Summary

let and const are block-scoped (live inside the nearest braces); var is function-scoped and leaks, which is why modern code avoids it. Hoisting moves declarations up — var becomes undefined early, let/const error if used before declaration. Closures are inner functions that retain access to their outer scope’s variables, creating private, persistent state — the mechanism behind module patterns and the way bundles hide config and tokens.

Key takeaways

  • let/const are block-scoped; var is function-scoped and leaks — prefer the former.
  • Hoisting makes var exist (as undefined) before its line; let/const error.
  • A closure is an inner function that remembers its outer variables.
  • Closures hold private state — including config and tokens inside bundles.

Quick quiz

Next, the asynchronous side of JavaScript — promises, async/await, and fetch — which is how the client talks to APIs.

Was this lesson helpful?