JavaScript has function scope (var), block scope (let/const), and module scope. Understanding which scope a variable lives in is critical to avoiding subtle bugs with callbacks, loops, and timers.
// Scope basics
let x = 1 // module scope
function outer() {
let y = 2 // function scope
function inner() {
let z = 3 // function scope of inner
console.log(x, y, z) // can see all three
}
inner()
// console.log(z) // ReferenceError — z doesn't exist here
}
// Block scope with let/const
{
let blockVar = "inside"
const blockConst = "also inside"
}
// console.log(blockVar) // ReferenceError
// Classic closure
function makeCounter(start = 0) {
let count = start // this variable lives in the closure
return {
increment() { count++; return count },
decrement() { count--; return count },
value() { return count },
reset() { count = start },
}
}
const counter = makeCounter(10)
console.log(counter.increment()) // 11
console.log(counter.increment()) // 12
console.log(counter.decrement()) // 11
console.log(counter.value()) // 11
// Two counters — separate closures, separate state
const c1 = makeCounter()
const c2 = makeCounter(100)
c1.increment()
console.log(c1.value(), c2.value()) // 1, 100
// The classic loop closure bug
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0)
}
// Prints: 3 3 3 — all callbacks share the same var i
// Fix 1: use let (block-scoped, new binding each iteration)
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0)
}
// Prints: 0 1 2
// Fix 2: IIFE to capture current value (old-school)
for (var i = 0; i < 3; i++) {
((captured) => {
setTimeout(() => console.log(captured), 0)
})(i)
}
// Practical closure: memoization
function memoize(fn) {
const cache = new Map()
return function(...args) {
const key = JSON.stringify(args)
if (cache.has(key)) return cache.get(key)
const result = fn.apply(this, args)
cache.set(key, result)
return result
}
}
const expensiveFib = memoize(function fib(n) {
if (n <= 1) return n
return expensiveFib(n - 1) + expensiveFib(n - 2)
})
console.log(expensiveFib(40)) // fast!
// Partial application via closure
const multiply = (a) => (b) => a * b
const double = multiply(2)
const triple = multiply(3)
console.log(double(5)) // 10
console.log(triple(5)) // 15
◆ Note
Use const by default, let when you need to reassign, and never use var. The var keyword ignores block scope, which causes exactly the kind of bugs that let and const were invented to prevent.