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.
Trace retained state
Explain why count remains available after setup returns.
Separate counters
Create independent state with separate setup calls.
Understand capture
Distinguish a live binding from a frozen snapshot.
Each setup call creates one retained count
Create a counter
makeCounter creates one count binding.
count = 0Return a function
The returned function keeps access to that binding.
nextCall it again
The same binding advances on each call.
1, then 2
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
1A 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
37Apply 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
falseRecognize 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());Apply the idea to a new situation
JavaScript · code for this question
let mark = 35;
const readMark = () => mark;
mark = 37;
console.log(readMark());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.