EduFlow
JavaScript Fundamentals
0%

Section 2 · Logic and Functions

Scope and Closures

18 min4 min readCode playground

Lesson at a glance

Predict where a variable can be used (block, function and global scope), avoid the old var pitfalls, and use closures to give functions private, remembered state such as an id counter.

  1. 1Blocks, functions and the global scope
  2. 2The trouble with var
  3. 3Closures: functions that remember

No video here — just the real thing: a written lesson, runnable examples, a quiz and practice.

Every expense in Pocket needs a unique id so you can edit or delete exactly that one later. The obvious approach is a counter at the top of the file:

JavaScript
let nextId = 1;
function createId() {
  return `exp-${nextId++}`;
}

It works, until some other part of the program, perhaps a “reset” feature written months later, sets nextId = 1 again and you get two expenses called exp-1. The counter is visible everywhere, so anything can break it. To protect it you need to understand scope: the rules for where a name can be seen.

Blocks, functions and the global scope

Every pair of curly braces creates a block, and let and const names live in the block where they are declared. Functions create a scope too. Names declared outside every block and function are in the global scope, visible everywhere.

JavaScript
const appName = "Pocket";           // global

function describe(amount) {
  const size = amount > 10000 ? "big" : "small";   // function scope
  if (size === "big") {
    const warning = "Think twice";  // block scope
    console.log(appName, size, warning);
  }
  // console.log(warning) here would throw a ReferenceError
}

describe(12000);

When the engine meets a name, it looks in the current scope first. If the name is not there, it looks in the enclosing scope, then the one around that, out to the global scope. If it still has not found it, you get ReferenceError: … is not defined. The lookup only goes outward: code inside a function can read the global appName, but code outside the function can never see size.

That is why the warning line is commented out. The block that declared warning has ended, so the name is gone.

Three nested boxes: the global scope holds appName, the describe function scope holds amount and size, and the if block holds warning. An arrow from the innermost block points outward to show that lookups go from inner to outer scopes. Global appName function describe amount, size if block warning look outward
Scopes nest, and lookups go outward

If an inner scope declares a name that already exists outside, the inner one shadows the outer one: inside the block, the name means the inner variable. It is legal, but two different things with one name make code hard to read, so pick distinct names.

The trouble with var

Before 2015, JavaScript only had var, and var ignores blocks: it is visible throughout the whole function it is in. That produces one famous bug. Here the loop schedules three functions to run a moment later with setTimeout:

JavaScript
for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log("var:", i), 0);
}
for (let j = 0; j < 3; j++) {
  setTimeout(() => console.log("let:", j), 0);
}

The var loop logs 3 three times, because there is only one i for the whole loop, and by the time the functions run it has reached 3. The let loop logs 0, 1 and 2, because let creates a fresh j for each pass and each function keeps its own. Those functions remembering variables is exactly what a closure is.

Closures: functions that remember

A function created inside another function can use the outer function’s variables. The surprise is that it keeps that access after the outer function has returned:

JavaScript
function createIdGenerator(prefix) {
  let count = 0;
  return () => {
    count++;
    return `${prefix}-${count}`;
  };
}

const nextExpenseId = createIdGenerator("exp");
const nextCategoryId = createIdGenerator("cat");

console.log(nextExpenseId());
console.log(nextExpenseId());
console.log(nextCategoryId());
console.log(nextExpenseId());

Follow the engine. Calling createIdGenerator("exp") creates a fresh prefix and count, builds the arrow function, and returns it. Normally a function’s variables vanish when it returns, but the arrow function still refers to count and prefix, so they stay alive, attached to it. That pairing of a function with the variables it was born next to is a closure.

Each call of createIdGenerator creates a new, separate set of variables. That is why nextCategoryId() starts from 1 and the expense ids carry on from where they were.

Now look at the safety you gained. There is no global count. No other code can read it, reset it or set it to a duplicate value. The only way to affect it is to call the function you were given, which can only ever move it forward. This is the standard way to give a function private state in JavaScript.

A closure can protect more than a counter. Here is a budget that can only go down through the function you hand out:

JavaScript
function createBudget(limitCents) {
  let remaining = limitCents;
  return (spentCents) => {
    remaining -= spentCents;
    return remaining;
  };
}

const spend = createBudget(2000);
console.log(spend(450));
console.log(spend(1220));

Section 2 is complete: you can make decisions, repeat work, and package it in functions with their own private state. Section 3 turns to data, starting with the list you have been looping over: the array.

Key takeaways

  • Scope is where a name can be used: let and const live in the nearest block { }, and lookups go outward, never inward.
  • An inner name with the same name as an outer one shadows it inside the inner scope, which is legal but easy to misread.
  • var ignores blocks and is scoped to the whole function, which causes classic bugs; use let and const.
  • A closure is a function that keeps access to the variables of the scope where it was created, even after that outer function has returned.
  • Each call of an outer function creates a fresh set of variables, so every closure it returns has its own private state.

Further reading

Check your understanding

3 questions

Question 1 of 3

What happens when this runs?

JavaScript
function check(amount) {
  if (amount > 100) {
    const label = "big";
  }
  return label;
}
console.log(check(500));
Question 2 of 3

What does this log?

JavaScript
function makeCounter() {
  let count = 0;
  return () => {
    count++;
    return count;
  };
}
const a = makeCounter();
const b = makeCounter();
a(); a();
console.log(a(), b());
Question 3 of 3

Why is a closure-based id generator safer than a global let nextId = 1?

Practice

Code playground

Private counters with closures

The starter’s id generator shares one global count, so two generators interfere and any code can reset it. Rewrite createIdGenerator(prefix) so each generator has its own private counter and there is no global count any more: a new generator’s first id is always prefix-1.

Then write createBudget(limitCents), which returns a function spend(cents). Each call subtracts cents from a private remaining amount (starting at limitCents) and returns what is left. It may go negative.

// A global counter: any code can break it. Move it into a closure.
let count = 0;

function createIdGenerator(prefix) {
  return () => {
    count++;
    return `${prefix}-${count}`;
  };
}

// Return a function spend(cents) that subtracts from a private
// remaining amount and returns what is left
function createBudget(limitCents) {
}

const nextId = createIdGenerator("exp");
console.log(nextId(), nextId());
Loading the editor…

Ctrl/⌘ + Enter to run · Runs in a sandbox in your browser. Nothing leaves your device.

Console

(no output)

Checks

  • A generator returns prefix-1, prefix-2, prefix-3…
  • Two generators keep separate counts
  • There is no global count variable any more
  • spend subtracts and returns what is left
  • Two budgets keep separate remaining amounts