Lesson 9 of 18 · 0 of 2 exercises solved
Closures & Scope
You will learn how decides where a can be used, and how a lets a keep access to variables from where it was created. , event handlers and many common patterns rely on this.
JavaScript has function scope (var), block scope (let/const), and scope. Understanding which scope a variable lives in is critical to avoiding subtle with callbacks, , 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)) // 15Common question
Does a variable disappear when its function returns?
Scope is the part of the code where a name is visible: global, function or block. A closure is a function together with the scope it was created in; when you call it later, it can still read and write those variables. That is what gives JavaScript private state without classes, callbacks that remember their context, and the module pattern.
Prefer let and const so each block and each loop iteration gets its own bindings. When a callback needs to remember something, let it close over a variable instead of using a global. And if several closures share a variable by accident, expect them to see each other's changes.
Source: The ECMAScript specification gives every function a to the environment it was created in. Closures are not an add-on; they are how JavaScript functions work.
Common mix-ups
Each of these sounds reasonable. Think about why it is wrong, then open the explanation.
Local variables live in the function's frame and vanish when it returns.
A closure captures the value a variable had when the closure was created.
Block scope and function scope are the same thing.
Practice
Write makeCounter() that returns a function; each call to that function returns the next number, starting at 1. The count must be private, and two counters must not share state.
A private counter
function makeCounter() {
// ...
}
const a = makeCounter()
const b = makeCounter()
console.log(a(), a(), b())
Press Run to try your code, then Check answer to grade it.A classic bug: every timeout logs 3 because var is function-scoped. Fix the loop so logged ends up as [0, 1, 2]. One keyword change is enough.
The loop closure bug
const logged = []
for (var i = 0; i < 3; i++) {
setTimeout(() => logged.push(i), 10)
}
await new Promise(r => setTimeout(r, 50))
console.log(logged)
Press Run to try your code, then Check answer to grade it.Check yourself
What is a closure?
Tip: use the ← and → keys to move between lessons.