Scope, hoisting & closures
Block, function and global scope, hoisting and the temporal dead zone, and closures.
Scope answers the question "where can I use this variable?". Hoisting explains what JavaScript does with declarations before running your code. Closures — functions remembering their surroundings — follow naturally and power everything from counters to React hooks.
Three levels of scope#
- Block scope:
letandconstlive inside the nearest{ }— anif, loop or plain block. - Function scope: variables declared in a function are private to it.
varonly respects function boundaries, not blocks. - Global scope: top-level variables. In a browser
<script>(not a module), top-levelvareven becomes a property ofwindow. ES modules have their own top-level scope, which is one reason to use them.
The scope chain
Inner scopes can see outer ones, but not the other way round. JavaScript looks up a name in the current scope, then the enclosing one, and so on out to the global scope:
Declaring a variable with the same name in an inner scope shadows the outer one. It's legal but can be confusing — prefer distinct names.
This is called lexical scope: what a function can see is decided by where it's written, not where it's called from.
Hoisting#
Before running code, JavaScript scans each scope and sets up its declarations. That's hoisting, and each kind of declaration behaves differently:
The zone between the start of the scope and the let/const line is the temporal dead zone (TDZ). It's a feature: an error is much better than a silent undefined.
Closures#
A closure is a function together with the variables it captured from where it was created. Every function in JavaScript is a closure:
makeCounter has finished running, yet the returned function still reads and updates count. Each call to makeCounter creates a fresh count, so the counters are independent. Nothing outside can touch count directly — closures give you private state.
Practical closure: a module with private data
Practical closure: configuring functions
The expensive formatter is created once and reused by the closure.
Practical closure: run only once
The classic loop trap#
With var there's one i shared by all callbacks, and by the time they run the loop has finished and i is 3. let creates a new binding for each iteration, so each callback closes over its own value. Yet another reason to avoid var.
Closures and memory#
A closure keeps its captured variables alive as long as the function itself is reachable. That's usually exactly what you want, but a long-lived closure (for example, an event listener that's never removed) that captures a huge array keeps that array in memory. Remove listeners and clear timers you no longer need.
Common mistakes#
- Using a
let/constvariable before its declaration (TDZ error). - Accidentally creating a global by assigning to an undeclared name (
total = 5). Modules and"use strict"turn this into an error — another reason to use modules. - Expecting inner variables to be visible outside their block.
- Shadowing an outer variable and then being confused about which one changed.
What's next#
With functions and scope under your belt, it's time to organise data with arrays and objects.
Check your understanding
Quick quiz
1.What happens when you run
console.log(x); let x = 1;?2.What is a closure?
3.What does this print?
for (var i = 0; i < 3; i++) setTimeout(() => console.log(i), 0);
Finished reading?
Mark this lesson complete to track your progress.