Closure patterns
Use closures to build IIFE modules, factory functions, once wrappers, memoized functions, and configurable function factories with private state.
- 01Hide state on purposeBuild an IIFE module and explain why its variables stay private.
- 02Create independent instancesUse factory functions for separate closures with separate data.
- 03Wrap behaviorImplement
once, memoization, and small function factories.
Patterns, not magic
The Closures lesson showed the core rule: a function keeps access to variables from the place where it was created. This lesson turns that rule into practical shapes you will recognize in real code: modules, factories, wrappers, caches, and custom function builders.
A closure pattern is a repeatable way to use remembered variables: hide state, create separate instances, allow one call, cache answers, or configure a returned function.
We will use a few ideas from earlier lessons: function expressions and callbacks, arrow functions, Map and Set for collection-shaped state, and WeakMap when object-keyed caches should not keep objects alive. Later lessons named Memoization & laziness and Currying & partial application go deeper. The Modules stage explains why ES modules replaced most old module-pattern code.
A pop-up shop appears, does its setup, serves customers, then leaves the street clear. An IIFE does the same for old scripts: setup code runs immediately, and local names stay off the global street.
- In real life: The shop is built in the morning
- In JavaScript: The function expression is created
- In real life: It sells what it needs to sell right away
- In JavaScript: The trailing
()calls it immediately - In real life: At closing time, the tables are packed away
- In JavaScript: Local variables do not become globals
- In real life: A small front counter may remain
- In JavaScript: A returned object can expose public methods
Where the analogy stops: A real pop-up disappears completely. An IIFE can deliberately leave behind returned functions that keep selected variables alive through closures.
IIFEs & the module pattern
STEP THROUGHIIFE means immediately invoked function expression. The classic shape is (function () { ... })();: parentheses turn the function into an expression, then the final parentheses call it. Before ES modules, this was a common way to avoid global pollution in browser scripts.
The module pattern adds one more step: return an object whose methods close over private variables. Think of a shop with a locked back room (count) and a front counter (the returned increment and get methods). Use inline methods in returned objects; that shape is also safer with production minifiers than wiring separately declared local functions onto a local object.
Step through an IIFE module. Watch setup run once, then use the returned API while count stays private.
script
let count = 0; console.log("IIFE ran once"); return { increment() { count += 1; return count; }, get() { return count; }, };})(); console.log(counterModule.count);console.log(counterModule.increment());console.log(counterModule.increment());console.log(counterModule.get());Notice the three important facts: setup printed once, counterModule.count was undefined, and the public methods still changed the private count. Today you usually choose ES modules for file-level privacy and imports, but the old pattern is still worth reading in legacy libraries. The Why modules? lesson later explains that history.
Factory functions
INTERACTIVEA factory function is a normal function that creates and returns a value, often an object. With closures, each factory call can create private state. A cookie cutter is a good picture: every cookie has the same shape, but each one can carry its own secret recipe card.
The same cutter can stamp out many cookies. A factory function can stamp out many objects. The closure part is the private card: each object’s methods remember the variables from the factory call that made it.
- In real life: One cutter shape
- In JavaScript: One
createAccountfunction - In real life: Many cookies
- In JavaScript: Many returned account objects
- In real life: A private card tucked under each cookie
- In JavaScript: Each call’s
ownerandbalancevariables - In real life: You interact with the cookie, not the card
- In JavaScript: Call
deposit,withdraw, andlabel
Where the analogy stops: Cookies do not usually change their recipe after baking. Closure-backed objects can update their private variables over time.
function createAccount(owner, balance) { return { deposit(amount) { balance += amount; return balance; }, withdraw(amount) { balance -= amount; return balance; }, getBalance() { return balance; }, label() { return owner + ": ₹" + balance; }, };} const ada = createAccount("Ada", 40);const grace = createAccount("Grace", 15);ada.deposit(10);grace.withdraw(5);console.log(ada.label());console.log(grace.label());console.log(ada.balance);Ada: ₹40undefinedGrace: ₹15undefinedDirect property check for Ada: undefined. That is why callers must use the returned methods.
Two accounts were created by two factory calls. Try changing one balance.
owner and balance variables. The buttons call real methods on real closure-backed account objects.This style is naturally this-free: the methods use the closed-over balance variable, not this.balance. That avoids binding mistakes when a method is passed around. The trade-off is memory: each factory call creates new closures, and this version creates new method functions per account. Classes with #private fields offer another privacy tool; a later Classes lesson goes deeper.
once & memoize
STEP + PLAYClosure wrappers are functions that take a function and return a new function with memory. once(fn) is a single-use ticket: the first call punches the ticket and saves the result; later calls return that first result. This is useful for one-time setup, connecting once, or making an initialization API idempotent.
Step through once(fn): the first call runs fn; later calls return that same first result.
script
function once(fn) { let called = false; let firstResult; return function (...args) { if (!called) { called = true; firstResult = fn(...args); } return firstResult; };} const welcome = once((name) => { runs += 1; return "Welcome, " + name;});console.log(welcome("Ada"));console.log(welcome("Grace"));console.log(runs);memoize(fn) is a notebook. When you solve a problem, you write down the answer under a key. The next time the same inputs arrive, you read from the notebook instead of doing the work again. In JavaScript, the notebook is often a Map in a closure.
function memoize(fn) { const cache = new Map(); return function (...args) { const key = JSON.stringify(args); if (cache.has(key)) return cache.get(key); const result = fn(...args); cache.set(key, result); return result; };}41 calls13 calls13Both versions return 13. The plain recursive version made 41 calls; the memoized version made 13 calls because cached subproblems are reused.
JSON.stringify(args) is fine for a small teaching utility, but it is not a perfect key system for every value. Object key order, unsupported values, and very large inputs need thought. Caches also grow unless you limit or clear them; the next lesson, Closures in loops, timers & memory, returns to memory held by closures.
Function factories
INTERACTIVEA function factory returns a configured function. It is a vending machine for tools: put in 3, get a “times three” function; put in "Hello", get a greeting function; put in a range, get a validator. The returned function remembers the configuration through closure.
function makeMultiplier(factor) { return function multiply(number) { return number * factor; };} function makeGreeting(greeting) { return function greet(name) { return greeting + ", " + name + "!"; };} function makeValidator({ min, max }) { return function isInRange(value) { return value >= min && value <= max; };} const triple = makeMultiplier(3);const hello = makeGreeting("Hello");const isChildAge = makeValidator({ min: 5, max: 12 });console.log(triple(7));console.log(hello("Ada"));console.log(isChildAge(9));21Hello, Ada!trueThe returned functions remember their configuration: factor 3, greeting "Hello", and range 5-12.
Function factories are a gentle bridge to more advanced functional programming. Currying & partial application later changes function shapes more formally. For now, focus on the practical habit: put configuration in the outer call, and put changing input in the returned function’s call.
(function () { setup(); })();- Return an object with methods that share a hidden
count. - createAccount('Ada', 40) returns a new account object.
- Run
connect()the first time, return the same result later. - Cache
fib(35)so the nextfib(35)returns immediately. - makeMultiplier(3) returns a custom
timesThreefunction.
Sort each card by the closest pattern. The categories are grouped because real code often combines them.
Where you’ll use this
Closure patterns appear whenever you want a small API with memory but do not want to expose the memory itself. UI components can keep counters, form helpers can keep validation settings, and data utilities can remember recent results. The pattern is not tied to the browser; it is plain JavaScript.
function makeIdGenerator(prefix) { let next = 1; return function id() { const value = prefix + next; next += 1; return value; };} function createToggle(initial = false) { let on = initial; return { flip() { on = !on; return on; }, value() { return on; }, };} const memoizedFormat = memoize((amount, currency) => new Intl.NumberFormat("en", { style: "currency", currency }).format(amount),);In each case, the caller gets a clean function or object and does not have to manage extra variables. That is the real power of the pattern: a tiny surface area with a remembered interior.
Factories vs classes
Factories and classes can both create many similar things. Pick the one that makes the state, API, and team conventions clearest.
| Question | Factory with closures | Class with private fields |
|---|---|---|
| Privacy | Private variables live in a closure and are unreachable as properties. | #private fields are enforced by the language on class instances. |
this | Can avoid this entirely by reading closed-over variables. | Methods usually use this, so detached calls need care. |
| Memory | Often creates new method functions per instance. | Prototype methods are shared; private fields store per-instance data. |
instanceof | Plain returned objects usually do not identify the factory. | Instances naturally work with instanceof ClassName. |
| Best fit | Small APIs, wrappers, configuration, and privacy without inheritance. | Many instances with shared behavior, subclassing, or class-based team style. |
Common misconceptions
- “An IIFE is asynchronous.” It is just a function call. It runs immediately and synchronously.
- “Private means secure from all code.” Closure privacy hides direct access, but public methods still decide what callers can do.
- “Factory calls share one state.” They share code, but each call creates a new lexical environment unless you intentionally close over shared outer data.
- “Memoize is always faster.” It helps repeated expensive calls, but it adds key creation, lookup cost, and memory use.
- “Function factories are currying.” They are related, but this lesson uses a broader practical idea. Currying & partial application is more specific.
- “Closures leak by default.” They keep reachable values alive. That is useful, but long-lived closures with large retained data need care.
Practice closure patterns
5 EXERCISESonce(fn)Implement the wrapper so the setup function runs only once. Then predict the three printed lines.
function once(fn) {
let called = false;
let firstResult;
return function (...args) {
if (!called) {
called = true;
firstResult = fn(...args);
}
return firstResult;
};
}
let calls = 0;
const init = once(() => {
calls += 1;
return "ready";
});
console.log(init());
console.log(init());
console.log(calls);function once(fn) {
let called = false;
let firstResult;
return function (...args) {
if (!called) {
called = true;
firstResult = fn(...args);
}
return firstResult;
};
}
let calls = 0;
const init = once(() => {
calls += 1;
return "ready";
});
console.log(init());
console.log(init());
console.log(calls);The returned function closes over called and firstResult. The original function runs once, so the printed lines are ready, ready, and 1.
Run the program and check the final printed call count. This proves behavior instead of guessing from timing.
function memoize(fn) {
const cache = new Map();
return function (...args) {
const key = JSON.stringify(args);
if (cache.has(key)) return cache.get(key);
const result = fn(...args);
cache.set(key, result);
return result;
};
}
let calls = 0;
const double = memoize((n) => {
calls += 1;
return n * 2;
});
console.log(double(4));
console.log(double(4));
console.log(double(5));
console.log(calls);function memoize(fn) {
const cache = new Map();
return function (...args) {
const key = JSON.stringify(args);
if (cache.has(key)) return cache.get(key);
const result = fn(...args);
cache.set(key, result);
return result;
};
}
let calls = 0;
const double = memoize((n) => {
calls += 1;
return n * 2;
});
console.log(double(4));
console.log(double(4));
console.log(double(5));
console.log(calls);double(4) computes once and caches 8. The second double(4) reuses it. double(5) is new, so the final call count is 2.
Create makeCounter(step) so two calls advance by the step and reset returns to zero.
function makeCounter(step) {
let count = 0;
return {
next() {
count += step;
return count;
},
reset() {
count = 0;
return count;
},
};
}
const byThree = makeCounter(3);
console.log(byThree.next());
console.log(byThree.next());
console.log(byThree.reset());function makeCounter(step) {
let count = 0;
return {
next() {
count += step;
return count;
},
reset() {
count = 0;
return count;
},
};
}
const byThree = makeCounter(3);
console.log(byThree.next());
console.log(byThree.next());
console.log(byThree.reset());next() adds the configured step, while reset() writes the private count back to 0 and returns it.
A page has let count = 0 at the top level and several scripts use window.count by mistake. Which variable should you hide?
const counter = (function () {
let count = 0;
return {
increment() {
count += 1;
return count;
},
get() {
return count;
},
};
})();The global count becomes private inside the IIFE. Only the returned methods can read or change it.
Write makeTax(rate) so different returned functions can add different tax rates.
function makeTax(rate) {
return function addTax(amount) {
return amount + amount * rate;
};
}
const addTenPercent = makeTax(0.10);
console.log(addTenPercent(50));function makeTax(rate) {
return function addTax(amount) {
return amount + amount * rate;
};
}
const addTenPercent = makeTax(0.10);
console.log(addTenPercent(50));makeTax(0.10) returns a function that remembers 10%. Calling it with 50 returns 50 + 5, so it prints 55.
Check your understanding
7 QUESTIONSQuestion 1 of 7What is the main reason to wrap old script setup in an IIFE?
Choose an answer to see the explanation.
Question 2 of 7What does this module-pattern code print?
Read the code, then predictconst module = (function () { let count = 0; return { inc() { count += 1; return count; } }; })(); console.log(module.count); console.log(module.inc());Choose an answer to see the explanation.
Question 3 of 7Each call to a factory function usually creates what?
Choose an answer to see the explanation.
Question 4 of 7What does this once-wrapper code print?
Read the code, then predictfunction once(fn) { let called = false; let firstResult; return (...args) => { if (!called) { called = true; firstResult = fn(...args); } return firstResult; }; } let calls = 0; const greet = once((name) => { calls += 1; return "Hi " + name; }); console.log(greet("Ada")); console.log(greet("Grace")); console.log(calls);Choose an answer to see the explanation.
Question 5 of 7What is a common caveat of simple
memoizewithJSON.stringify(args)?Choose an answer to see the explanation.
Question 6 of 7What does this function-factory code print?
Read the code, then predictfunction makeMultiplier(factor) { return (number) => number * factor; } const double = makeMultiplier(2); const triple = makeMultiplier(3); console.log(double(5)); console.log(triple(5));Choose an answer to see the explanation.
Question 7 of 7Which statement about factory objects is safest?
Choose an answer to see the explanation.
Key takeaways
- IIFEs create a temporary scope; module patterns return a public API over private variables.
- Factory functions can create independent closures with private per-instance state.
onceandmemoizeare wrappers that remember control state or cached answers.- Function factories put configuration in an outer call and return a custom reusable function.
- Closures are powerful, but every retained value has a memory cost until it is no longer reachable.
Closure patterns are reusable shapes where returned functions remember private variables to make safer, smaller APIs.
Up next: Closures in loops, timers & memory.