← Back to JavaScript series
🔒
Functional · ES6
Intermediate · Prerequisite: 11_Higher_Order_Functions

12. Closures and Scope

A function's scope is determined by **where it was declared**, not where it was called.

closurescopelexicalencapsulation
Duration
~1.5 hours
Level
📊 Intermediate
Prerequisite
🎯 Lecture 11
OUTCOME
A function's scope is determined by **where it was declared**, not where it was called.

What you'll learn

  • 1Explain lexical scope.
  • 2Know what a closure is and when one is created.
  • 3Use a closure to build a counter with private state.
  • 4Use the module pattern (IIFE) to split public from private members.
  • 5Fix the classic loop-variable capture bug with `let`.

Overview

A function's scope is determined by **where it was declared**, not where it was called.

Core Concepts

1. Lexical scope

A function's scope is determined by **where it was declared**, not where it was called.

javascript
const x = 1;
function outer() {
  const y = 2;
  function inner() {
    return x + y; // references the outer/global variable
  }
  return inner;
}

2. Closures

A closure is what we call it when a function remembers the variables of the environment it was created in. Even after `outer()` returns, `inner` can still use `y`.

javascript
const fn = outer();
fn(); // 3

3. Private state: a counter

Closures let you create state that nothing outside the function can touch.

javascript
function makeCounter() {
  let count = 0;
  return {
    inc() { count += 1; return count; },
    get() { return count; },
  };
}
const c = makeCounter();
c.inc(); c.inc(); c.get(); // 2

4. Module pattern (IIFE)

Immediately-invoked function expressions create a one-shot scope and return only the parts you want to expose.

javascript
const Logger = (() => {
  let level = "info";
  function setLevel(l) { level = l; }
  function log(msg) { console.log(`[${level}] ${msg}`); }
  return { setLevel, log };
})();

Examples

FileWhat it covers
`01_scope.js`Block/function scope, let/const/var
`02_closure.js`Closure basics
`03_counter.js`Counter with private state
`04_module_pattern.js`IIFE module pattern

src/01_scope.js

javascript
/**
 * Scope
 */
"use strict";

const g = "global";

function outer() {
  const o = "outer";
  if (true) {
    const b = "block";
    console.log(g, o, b);
  }
  // console.log(b); // ReferenceError
}

outer();

// let is block-scoped
for (let i = 0; i < 3; i++) {
  setTimeout(() => console.log("i =", i), 0);
}

src/02_closure.js

javascript
/**
 * Closure basics
 */
"use strict";

function makeGreeter(greeting) {
  return function (name) {
    return `${greeting}, ${name}!`;
  };
}

const hi = makeGreeter("Hi");
const hello = makeGreeter("Hello");
console.log(hi("Sun"));
console.log(hello("Moon"));

src/03_counter.js

javascript
/**
 * Counter with private state
 */
"use strict";

function makeCounter(start = 0) {
  let count = start;
  return {
    inc() { return ++count; },
    dec() { return --count; },
    get() { return count; },
  };
}

const c = makeCounter(10);
c.inc();
c.inc();
c.dec();
console.log("count =", c.get()); // 11

src/04_module_pattern.js

javascript
/**
 * IIFE module pattern
 */
"use strict";

const Logger = (() => {
  let level = "info";
  function setLevel(l) { level = l; }
  function log(msg) { console.log(`[${level}] ${msg}`); }
  return { setLevel, log };
})();

Logger.log("start");
Logger.setLevel("warn");
Logger.log("careful");

Common Mistakes

  1. Capturing a `var` loop variable in a `setTimeout` callback and getting the wrong value — use `let`.
  2. Forgetting that closures keep their captured variables alive and GC can't reclaim them (mind large data).
  3. Trying to mutate a closure variable from outside (you can't).
  4. Confusing function declaration hoisting with expression hoisting.
  5. Shadowing an outer variable with one of the same name inside and getting confused.

FAQ

Q1. When is a closure created?

Whenever a function definition references a variable from its surrounding scope.

Q2. Are closures bad for performance?

Generally negligible — but holding on to large objects can leak memory.

Q3. How do `let` and `const` define block scope?

Their visibility is bounded by `{ ... }`.

Practice

  • `homework_01.js` — Write `makeAdder(n)` returning a closure that adds `n`.
  • `homework_02.js` — Write `makeAccount` with a private balance.

homework/README.md

## homework_01.js — Write `makeAdder(n)`. Calling it returns a function that takes `x` and returns `x + n`.

## homework_02.js — Write `makeAccount(initial)`.

  • Expose `deposit(amount)`, `withdraw(amount)`, `balance()`.
  • The balance must not be reachable from outside.

homework/answer/homework_01.js

javascript
/**
 * Homework 1: makeAdder
 */
"use strict";

/**
 * @param {number} n
 */
function makeAdder(n) {
  return (x) => x + n;
}

const add5 = makeAdder(5);
console.log(add5(3));  // 8
console.log(add5(10)); // 15

homework/answer/homework_02.js

javascript
/**
 * Homework 2: account with private balance
 */
"use strict";

/**
 * @param {number} initial
 */
function makeAccount(initial) {
  let balance = initial;
  return {
    deposit(amount) { balance += amount; },
    withdraw(amount) {
      if (amount > balance) throw new Error("insufficient");
      balance -= amount;
    },
    balance() { return balance; },
  };
}

const acc = makeAccount(1000);
acc.deposit(500);
acc.withdraw(300);
console.log(acc.balance()); // 1200

Next Lecture

[13_Classes](../13_클래스/) — OOP with class syntax.

Example code / lecture materials

All lecture materials and example code are openly available on GitHub.

View on GitHub ↗