Closures at Work: Private State and Factories
Put closures to work to keep a count that no other code can change, to build as many independent counters as you need with a factory function, and to make a function that runs only once.
Journeyman · JavaScript · Updated
After this lesson you can
- Write a factory function that keeps private state in a closure
- Write a wrapper function that lets another function run only once
Key takeaways
- Any line in a file can change a variable at its top, even by mistake. A variable kept in a closure can change only through the functions that share it.
- Data kept this way is private state. Those functions are the only way in, so they can enforce the rules, such as never going below zero.
- A factory function makes and returns a new object each time it's called. Each object gets its own private variables.
- A wrapper function can keep a done flag in a closure, so the function it wraps runs only the first time.
A payment button that must never charge a card twice, however many times an impatient shopper clicks it. A game where every player has their own lives, and no bug in some other part of the code can hand out 99 of them. A shopping cart that only lets items in through an "add" button that checks the stock. Each of these needs data that only the right code can change, and closures are one of JavaScript's main tools for it. This lesson puts the closures from Closures: Functions That Remember to work.
The trouble with a variable anyone can change
The arcade's Space Blaster gives each player 3 lives. Here they're kept in a variable at the top of the file:
let lives = 3;
const loseLife = () => {
lives = lives - 1;
};
loseLife();
lives = 99;
console.log(`Lives left: ${lives}`);
Lives left: 99
loseLife is careful, but it isn't the only code that can reach lives. Any line in the file can, and here one does: maybe a typo, maybe a bug in a part of the program written months later. In a file of 30 lines you'd spot it. In a game of 3,000 lines, you'd spend an evening hunting for it.
A lockbox: private state in a closure
Picture a lockbox with a few slots in its lid: one to drop a coin in, one to check the count. You can't open the lid; you can only use the slots. Move lives inside a function, and it becomes that lockbox. The function returns an object of small arrow functions (the slots), and they're the only code that can reach the variable, because they're the only code written in its scope:
function makeLivesCounter() {
let lives = 3;
return {
loseLife: () => {
if (lives > 0) {
lives = lives - 1;
}
return lives;
},
extraLife: () => {
lives = lives + 1;
return lives;
},
livesLeft: () => lives,
};
}
const ava = makeLivesCounter();
ava.loseLife();
ava.loseLife();
ava.extraLife();
console.log(`Lives left: ${ava.livesLeft()}`);
console.log(ava.lives);
Lives left: 2
undefined
The three arrow functions are closures: they all carry the same lives in their backpack. ava.lives is undefined, because the object has no property by that name; the variable lives in the closure, not in the object. Data that only its own functions can reach is called private state ("state" is the values a thing holds right now), and keeping data behind the functions that change it is called encapsulation.
Being the only way in is what makes the functions useful: loseLife checks lives > 0, so the count can never go below zero, and no other code can get around that rule.
A factory: one function, many players
makeLivesCounter is a factory function: a function that makes and returns a new object each time it's called, the way a factory turns out one toy after another from the same mold. Each call runs the function again, so each object gets its own private variables. A parameter lets each one start differently:
function makeLivesCounter(startingLives) {
let lives = startingLives;
return {
loseLife: () => {
if (lives > 0) {
lives = lives - 1;
}
return lives;
},
livesLeft: () => lives,
};
}
const ava = makeLivesCounter(3);
const leo = makeLivesCounter(5);
ava.loseLife();
ava.loseLife();
ava.loseLife();
ava.loseLife();
leo.loseLife();
console.log(`Ava: ${ava.livesLeft()}, Leo: ${leo.livesLeft()}`);
Ava: 0, Leo: 4
Ava lost four lives but stops at 0, and Leo's count is untouched by any of it. Ten players, ten calls, ten lockboxes. (The next chapter, Objects, Classes and this, shows the other way JavaScript builds many objects from one design: classes.)
Try it. Save the code as lives.js, add an extraLife function to the object, give Leo an extra life, and run node lives.js. Leo should end with 5.
A function that runs only once
The arcade hands each new player a coupon for one free game. The coupon must work the first time and never again. A closure can remember whether it has been used:
function makeFreePlayCoupon(game) {
let used = false;
return () => {
if (used) {
return 'This coupon has already been used';
}
used = true;
return `One free game of ${game}!`;
};
}
const redeem = makeFreePlayCoupon('Space Blaster');
console.log(redeem());
console.log(redeem());
console.log(redeem());
One free game of Space Blaster!
This coupon has already been used
This coupon has already been used
The same idea works for any function. A wrapper is a function that takes another function and returns a new one that adds something around it. This one adds "only the first time": it keeps a done flag in its closure, and calls the function it was given only while done is still false.
function once(action) {
let done = false;
return () => {
if (!done) {
done = true;
action();
}
};
}
const ringBell = () => console.log('Ding! A new high score!');
const ringBellOnce = once(ringBell);
ringBellOnce();
ringBellOnce();
ringBellOnce();
console.log('The game goes on.');
Ding! A new high score!
The game goes on.
!done is read "not done": it's true while done is still false. The bell rings once, however many times ringBellOnce is called. A real payment button does exactly this, so a double click charges the card one time. This version throws away whatever action returns; in Exercise: Arcade Token Counters you'll write a once that remembers the first result and hands it back every time.
Check yourself
2 questions to try on the lesson page.
So what
Private state, factories and run-once functions are closures doing real work: keeping data safe, making many independent copies, and enforcing rules no other code can skip. You'll meet the same patterns in event handlers, in libraries such as React, and later in this course in debounce, which waits for you to stop typing before it searches.
Key terms
- Closure
- Encapsulation
- Factory Function
- State