cf.completefrontendCode editorOpen lab
THE JAVASCRIPT FIELD GUIDE

Closure patterns

Use closures to build IIFE modules, factory functions, once wrappers, memoized functions, and configurable function factories with private state.

By the end, you can
  • 01
    Hide state on purposeBuild an IIFE module and explain why its variables stay private.
  • 02
    Create independent instancesUse factory functions for separate closures with separate data.
  • 03
    Wrap 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.

The short version

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.

Real-life analogyAn IIFE is a pop-up shop

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 THROUGH

IIFE 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.

Trace an IIFE module
Step 0 of 8Ready
Your turn: follow the blue line

Step through an IIFE module. Watch setup run once, then use the returned API while count stays private.

Running in
  1. script
Next: line 1
Click the blue line to take the next stepPop out in the code editor (opens in a new tab)JavaScript
  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());
CallStoreChangeResultRun = next line. Ran = already executed.
Recent returnsNothing yet. Start with the blue line.
A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.

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

INTERACTIVE

A 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.

Real-life analogyA factory is a cookie cutter with recipe cards

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 createAccount function
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 owner and balance variables
In real life: You interact with the cookie, not the card
In JavaScript: Call deposit, withdraw, and label

Where the analogy stops: Cookies do not usually change their recipe after baking. Closure-backed objects can update their private variables over time.

Create private bank accounts
Try it yourself
Choose an account:

Two accounts were created by two factory calls. Try changing one balance.

Each factory call creates fresh 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 + PLAY

Closure 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.

Trace once(fn)
Step 0 of 9Ready
Your turn: follow the blue line

Step through once(fn): the first call runs fn; later calls return that same first result.

Running in
  1. script
Next: line 13
Click the blue line to take the next stepPop out in the code editor (opens in a new tab)JavaScript
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);
CallStoreChangeResultRun = next line. Ran = already executed.
Recent returnsNothing yet. Start with the blue line.
A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.

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.

Memoize by counting calls
A simple memoize utilityPop out in the code editor (opens in a new tab)JavaScript
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;  };}
Call countsfib(7)
Plain recursion41 calls
Memoized recursion13 calls
Result13
Try it yourself
Pick n for fib(n)

Both versions return 13. The plain recursive version made 41 calls; the memoized version made 13 calls because cached subproblems are reused.

This compares counters, not timing. Small demos are clearer when you count real function calls.
Cache keys and memory

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

INTERACTIVE

A 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.

Build custom tools from functions
Three function factoriesPop out in the code editor (opens in a new tab)JavaScript
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));
Custom toolslive output
multiplier(7)21
greeter("Ada")Hello, Ada!
validator(9)true
Try it yourself

The returned functions remember their configuration: factor 3, greeting "Hello", and range 5-12.

A function factory is a vending machine for functions: choose settings first, then use the custom tool many times.

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.

Which closure pattern is this?
  • (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 next fib(35) returns immediately.
  • makeMultiplier(3) returns a custom timesThree function.
Try it yourself
0 of 6 correct

Sort each card by the closest pattern. The categories are grouped because real code often combines them.

Choose a category for every card. You can change an answer at any time; Reset clears them all.

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.

Three practical closure shapesPop out in the code editor (opens in a new tab)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.

Factory functions compared with classes
QuestionFactory with closuresClass with private fields
PrivacyPrivate variables live in a closure and are unreachable as properties.#private fields are enforced by the language on class instances.
thisCan avoid this entirely by reading closed-over variables.Methods usually use this, so detached calls need care.
MemoryOften creates new method functions per instance.Prototype methods are shared; private fields store per-instance data.
instanceofPlain returned objects usually do not identify the factory.Instances naturally work with instanceof ClassName.
Best fitSmall 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 EXERCISES
Exercise 1 · Warm-upWrite once(fn)

Implement the wrapper so the setup function runs only once. Then predict the three printed lines.

Starter codePop out in the code editor (opens in a new tab)JavaScript
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);

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · PracticeMemoize and prove call counts

    Run the program and check the final printed call count. This proves behavior instead of guessing from timing.

    Starter codePop out in the code editor (opens in a new tab)JavaScript
    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);

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticeBuild a counter with step and reset

      Create makeCounter(step) so two calls advance by the step and reset returns to zero.

      Starter codePop out in the code editor (opens in a new tab)JavaScript
      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());

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 4 · ChallengeConvert a global script into an IIFE module

        A page has let count = 0 at the top level and several scripts use window.count by mistake. Which variable should you hide?

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 5 · ChallengeCreate a tax-rate function factory

          Write makeTax(rate) so different returned functions can add different tax rates.

          Starter codePop out in the code editor (opens in a new tab)JavaScript
          function makeTax(rate) {
            return function addTax(amount) {
              return amount + amount * rate;
            };
          }
          const addTenPercent = makeTax(0.10);
          console.log(addTenPercent(50));

          Answer, then press Check. Spacing and letter case don’t matter.

            Check your understanding

            7 QUESTIONS
            Closure patterns quiz · 7 questionsScore: first tries count
            1. Question 1 of 7What is the main reason to wrap old script setup in an IIFE?

              Choose an answer to see the explanation.

            2. Question 2 of 7What does this module-pattern code print?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              const 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.

            3. Question 3 of 7Each call to a factory function usually creates what?

              Choose an answer to see the explanation.

            4. Question 4 of 7What does this once-wrapper code print?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              function 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.

            5. Question 5 of 7What is a common caveat of simple memoize with JSON.stringify(args)?

              Choose an answer to see the explanation.

            6. Question 6 of 7What does this function-factory code print?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              function 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.

            7. 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.
            • once and memoize are 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.

            CompleteFrontend Clear concepts. Working examples.