JSJavaScript · Lesson 9 of 14

Closures & Scope

A closure is a function that remembers the variables from the scope it was created in, even after that scope has ended. This sounds academic. It powers half of JavaScript.

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.

JavaScript
// 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
JavaScript
// 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.