Scope & Closures
Goals: understand the scope chain and variable lookup; be able to explain "what a closure is and what it's for". The number one interview question.
Scope
Scope = where a variable is visible. JS has three kinds:
const global = 'global scope: visible everywhere'
function fn() {
const fnScope = 'function scope: visible only inside the function'
}
{
let blockScope = 'block scope: let/const variables live only inside the {}'
}var has no block scope — the main reason it was retired:
for (var i = 0; i < 3; i++) {}
console.log(i) // 3 — var leaked outside
for (let j = 0; j < 3; j++) {}
// console.log(j) // ❌ ReferenceError — let behaves properlyThe Scope Chain
Inner scopes can read outer variables, never the reverse. Lookup goes inside-out, stopping at the first match:
const a = 1
function outer() {
const a = 2
function inner() {
console.log(a) // 2: checks itself, then outer — never reaches global
}
inner()
}
outer()Closures
A closure = an inner function plus the outer variables it references. When the inner function outlives the outer one (returned, or registered as a callback), the variables it references survive with it — that's a closure.
function createCounter() {
let count = 0 // this variable is "closed over"
return function () {
count++
return count
}
}
const counter = createCounter()
counter() // 1
counter() // 2 ← count wasn't destroyed; it remembers
counter() // 3
const counter2 = createCounter()
counter2() // 1 ← each createCounter call makes an independent closureWhat closures are for
- Private variables:
countcannot be read or tampered with from outside — only through the returned function; - Keeping state alive: normal function variables die when the call ends; closures preserve them;
- Everywhere in practice: debounce & throttle, callbacks carrying data — and Vue 3's
setupfundamentally relies on closures too.
The interview-ready answer
A closure means a function can access variables from the scope where it was defined, even when executed outside that scope. Typical uses are private variables and preserved state. The trade-off: referenced variables can't be garbage-collected, so overuse can leak memory.
Hoisting (Good to Know)
var declarations and function declarations are "hoisted" to the top of their scope:
console.log(a) // undefined (no error — the var declaration hoisted, the assignment didn't)
var a = 1
console.log(b) // ❌ ReferenceError (let has a "temporal dead zone")
let b = 2
fn() // ✅ function declarations hoist entirely — callable before defined
function fn() {}Practical takeaway: use let/const and declare before use, and hoisting can never bite you.
Exercises
- Predict the output:
let x = 10
function test() {
let x = 20
{
let x = 30
console.log(x)
}
console.log(x)
}
test()
console.log(x)- Write
createWallet(initial): returns{ deposit(n), getBalance() }; the balance must not be directly accessible from outside. - The classic: why does this print three
3s, and how do you fix it?
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100)
}Reference answer (Q3)
var creates one shared i; by the time the timer callbacks run, the loop has long finished and i is 3.
Fix: change var to let — let creates a fresh block-scoped variable each iteration, each callback closes over its own i, printing 0, 1, 2. This one question tests scope, closures, and async at once — an interview regular.