Closure: The Mechanism You've Already Been Using
The precise definition of closure, why it has nothing to do with purity by itself, and where this series already used it without naming it.
Move a function out of the scope it was written in, and call it from somewhere else entirely, logic says it should lose access to whatever variables it used to see. It doesn't.
Closure is when a function remembers a variable from the scope it was defined in, even after that function runs somewhere else completely. That's the whole mechanism, and it's worth pinning down precisely.
Seeing It Work
function makeOrderCounter() {
let count = 0;
return function next() {
count += 1;
return count;
};
}
const nextOrderNumber = makeOrderCounter();
nextOrderNumber(); // 1
nextOrderNumber(); // 2
nextOrderNumber(); // 3Normally, once a function finishes running, everything it created disappears. makeOrderCounter runs once, right there on line 1, and then it's done forever.
But look at line 8 onward: nextOrderNumber can still see count, and still change it, every single time you call it, long after makeOrderCounter finished. count should have disappeared. It didn't, because next kept a private link to it. That's closure: a variable staying alive inside one function, kept there by another function that remembers it.
Closure Says Nothing About Purity
Call nextOrderNumber() three times with the exact same input, none at all, and you get three different answers. That's a direct violation of same input, same output, the core rule this series spent a whole post establishing.
Closure itself is neutral. It's just memory. Whether a closure is pure or not depends entirely on what it's remembering.
let count = 0;
function next() {
count += 1;
return count;
}Same input, different output every call.
function makeDiscounter(rate) {
return function discount(price) {
return price - price * rate;
};
}Same input, same output, forever.
You Were Already Doing This
Look back at makeDiscounter in the green card above. It's not a new example. It's the exact same function from several posts ago, back when this series was only talking about shrinking surface area. discount closing over rate was closure the entire time, just not named yet.
unary from the Argument Adapters chapter is the same trick, just with a different kind of value:
function unary(fn) {
return function (arg) {
return fn(arg);
};
}The inner function remembers fn, the same way discount remembers rate. The only difference is what's being remembered, a function instead of a number, not the mechanism doing the remembering.
The One Rule That Actually Matters
Closing over a variable only stays safe for purity if that variable never gets reassigned. rate in makeDiscounter never changes after it's handed in, so every call built from it stays predictable. count in the counter example changes on purpose, every single call, so nothing built from it can ever be pure.
You'll use closure constantly from here on, sometimes to build something impure on purpose, like a counter genuinely needs to be, and sometimes to build something that looks like it depends on the outside world but is actually as predictable as a constant. Knowing which one you're doing is the whole difference.
Try It Yourself
Build a stringBuilder that keeps remembering every string it's given:
const greeting = stringBuilder('Hello');
const greetingWorld = greeting(' World');
greetingWorld(); // "Hello World"
const question = stringBuilder('How');
question(' are'); // returns a new function, doesn't print anything yet
question(' you?');
question(); // "How are you?"Calling stringBuilder (or the function it returns) with a string adds it to a running total. Calling it with nothing returns the combined string so far. The trick: each call needs to remember every piece that came before it, and different calls need to stay independent of each other, exactly the two things closure is built to do.
Keep reading