JavaScript › Core JavaScript
Scope, hoisting, and closures
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?
Because the returned inner function is a closure — it captured the count variable from makeCounter's scope. That single count variable persists as long as the returned function exists, so each call sees and updates the same count rather than a fresh one. The state lives in the closure.
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/constare block-scoped;varis function-scoped and leaks — prefer the former.- Hoisting makes
varexist (asundefined) before its line;let/consterror. - 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.