Closures and Higher-Order Functions

Give each class its own registration counter

Two school classes need independent counters. A single global count would mix them together, but passing a count through every button handler would also be awkward.

A closure is a function together with access to the bindings from its surrounding scope. A higher-order function receives or returns functions. Returning a counter function can retain one class’s count after setup finishes.

  1. Trace retained state

    Explain why count remains available after setup returns.

  2. Separate counters

    Create independent state with separate setup calls.

  3. Understand capture

    Distinguish a live binding from a frozen snapshot.

Each setup call creates one retained count

  1. Create a counter

    makeCounter creates one count binding.

    count = 0
  2. Return a function

    The returned function keeps access to that binding.

    next
  3. Call it again

    The same binding advances on each call.

    1, then 2
Read these stages alongside the complete example. They explain the relationship; they do not execute code.

The returned function can still reach its enclosing binding

makeCounter runs and creates a local count. It returns an arrow function whose body uses count. That returned function retains access to this particular call’s binding; the state is not a global variable.

A second makeCounter call creates a separate binding. This makes the factory higher-order because it returns a function. The returned counter is stateful, so it is not pure: identical calls can produce different results.

Before starting, review local scope and name lookup. These examples use JavaScript; other languages can use different syntax and rules.

Trace two independent class counters

classA retains the count created by the first setup call. Its calls return 1 and 2. classB belongs to another setup call and starts at 1.

Returning a function does not run its body yet. The increment happens only when classA() or classB() is called.

JavaScript · local teaching example

function makeCounter() {
  let count = 0;
  return () => {
    count += 1;
    return count;
  };
}
const classA = makeCounter();
const classB = makeCounter();
console.log(classA());
console.log(classA());
console.log(classB());

Expected output

1
2
1

A closure reads a binding when it runs

readMark captures access to mark, not a permanently frozen value of 35. Updating the binding before calling readMark means the later call returns 37.

JavaScript · local teaching example

let mark = 35;
const readMark = () => mark;
mark = 37;
console.log(readMark());

Expected output

37

Apply the rule to a small change

Create a function that remembers a pass threshold. Make one checker for 25 and another for 40, then test mark 35.

Worked answer

Each makePassCheck call retains its own threshold. The same mark passes the 25 threshold and fails the 40 threshold. Unlike the counter, these checkers do not change their captured threshold.

JavaScript · local teaching example

function makePassCheck(threshold) {
  return mark => mark >= threshold;
}
const passes = makePassCheck(25);
const excellent = makePassCheck(40);
console.log(passes(35));
console.log(excellent(35));

Expected output

true
false

Recognize mistakes and limits

Closures retain bindings that can change. When scheduling later work in loops, understand whether every callback shares one binding or receives a separate block-scoped let binding.

Retained state remains reachable while a retained function needs it. Capturing a large report unnecessarily can keep that data alive; release obsolete callbacks and capture only what is useful.

Keeping a variable inaccessible through the ordinary calling interface is helpful for organization, but it does not replace authorization or validation for school data.

For the language or tool rules used here, see JavaScript function behavior.

Check the core idea

JavaScript · code for this question

function makeCounter() {
  let count = 0;
  return () => {
    count += 1;
    return count;
  };
}
const classA = makeCounter();
const classB = makeCounter();
console.log(classA());
console.log(classA());
console.log(classB());
Why does classB start at 1 after classA returned 2?
Choose one answer. You can try again.

Answer and explanation

Each makeCounter call creates its own count binding.

Correct. The two returned functions retain different state.

Apply the idea to a new situation

JavaScript · code for this question

let mark = 35;
const readMark = () => mark;
mark = 37;
console.log(readMark());
What will readMark return after mark changes to 37?
Choose one answer. You can try again.

Answer and explanation

37 because the binding now contains that value.

Correct. Capture does not automatically create a snapshot.

Follow the binding retained by each function

Higher-order functions work with functions as inputs or results. Closures retain access to enclosing bindings, allowing useful factories while requiring care with shared changes and data lifetime.

Connect this idea with functions passed as values and stateful and pure behavior.

Reaching the recap and completing the quizzes are separate milestones.

    Next lesson: Work That Finishes Later →