Course outline · 0% complete

0/27 lessons0%

Course overview →

Private state and the module pattern

lesson 1-3 · ~12 min · 3/27

The module pattern

In lesson 1-2 a closure returned one function. Return an object of functions instead, and they all share the same closed-over variables.

That is the module pattern. A maker function whose local variables become private state, and whose returned object is the public API.

The sharing is the new part. Every method in the returned object was created during the same call, so every method sees the same variables, which is what makes them behave like one object's fields.

Nothing outside can touch balance directly. There is no account.balance property at all, and the only way in is through the functions you chose to expose.

This pattern is everywhere in real code, including database clients that hide their connection state, caches that hide their storage, and ID generators that hide their counter.

Before class syntax existed this was the way JavaScript libraries organized themselves. It is still the cleanest choice when one object needs genuinely private state, and it is worth comparing to the #private class fields you will meet in unit 3.

A bank account with no balance property

Two methods share one hidden variable.

function createAccount() {
  let balance = 0;
  return {
    deposit(amount) {
      balance += amount;
      return balance;
    },
    getBalance() {
      return balance;
    },
  };
}

const account = createAccount();
account.deposit(50);
account.deposit(25);
console.log(account.getBalance());
console.log(account.balance);

Output

75
undefined

balance is a local variable rather than a property, so reading account.balance gives undefined while the real balance stays untouched.

Both methods close over the same balance, which is why deposit and getBalance agree. They were created during one call to createAccount.

Calling createAccount() again produces a second account with its own balance, exactly as two counters did in the previous lesson.

Object.keys(account) returns ["deposit", "getBalance"] and nothing else, so the private variable does not leak through inspection either.

The shorthand deposit(amount) { ... } inside an object literal is just a method definition. It means the same thing as deposit: function (amount) { ... }.

It returns 75, because the assignment created an unrelated property.

account.balance = 9999 adds a new property named balance to the object. The object had no such property before, and now it has one.

getBalance never reads properties. It reads the closed-over local variable, which is still 75.

So the object now carries two unrelated things that happen to share a name, a public property nothing reads and a private variable everything uses.

That separation is the whole point of the pattern. Outside code cannot corrupt the state, because it cannot reach the state, and the worst it can do is litter the object with ignored properties.

The lesson for API design is that state is only as private as the mechanism protecting it. A leading underscore on _balance documents an intention, and a closed-over variable enforces it.

Memoize

A cache is a store of already-computed results you can return instead of recomputing them.

memoize(fn) wraps a function with a private cache object. The closure records each result under its input, and a repeat call returns the stored answer without running fn again.

The cache is a closed-over variable, so no other code can read or corrupt it. That is the same privacy balance had, applied to performance instead of correctness.

This matters when fn is expensive and gets called with repeated inputs, which describes a lot of real work, including layout measurements, parsing, and recursive computations.

There is a real tradeoff worth stating. The cache never shrinks, so memoizing a function called with thousands of distinct inputs trades a memory leak for the speed, and production versions bound the size.

The technique also assumes fn is pure, meaning the same input always produces the same output. Memoizing a function that reads the clock or a database returns stale answers forever.

makeCounter() closurelet count = 0private, no outside referenceincrement()value()returned objectthe only way inblockedOutside code can call the methods, and cannot read or write count directly.
The module pattern: count lives inside the closure, and only the returned methods can reach it.

A cache nothing else can reach

The same call runs twice and computes once.

function memoize(fn) {
  const cache = {};
  return function (x) {
    if (x in cache) {
      console.log("cache hit");
      return cache[x];
    }
    console.log("computing");
    cache[x] = fn(x);
    return cache[x];
  };
}

const square = memoize((n) => n * n);
console.log(square(4));
console.log(square(4));

Output

computing
16
cache hit
16

The first square(4) computes and stores, and the second finds 16 already in the closed-over cache.

Nothing outside can touch cache. There is no property to poke at, only the wrapped function.

cache is declared with const even though its contents change, because the binding never gets reassigned. Only the object it points to is mutated.

The x in cache check is deliberate rather than stylistic. Testing if (cache[x]) would recompute whenever the cached answer is 0, "", or false, since those are falsy but perfectly valid results.

Object keys are strings, so square(4) and square("4") share one entry here. A real implementation uses a Map, which keeps key types distinct.

Closures capture variables, not values

This is the closure trap interviewers reach for most often.

A closure holds a link to the variable, so it sees the variable's latest value rather than the value from when the closure was created.

With var, a loop has one shared i. Every closure created in the loop links to that same variable, which ends up at 3 once the loop finishes.

With let, each loop iteration gets a fresh i, so each closure captures its own and the values stay separate.

The reason is scoping rather than closures. var is function-scoped, so there is one binding for the whole function, while let is block-scoped and the loop body is a new block on every pass.

Before let existed, the fix was to create a scope by hand by wrapping the body in an immediately-invoked function so each iteration passed i in as an argument. Seeing that pattern in old code is a sign of exactly this problem.

var and let in the same loop

One keyword changes the output completely.

const withVar = [];
for (var i = 0; i < 3; i++) {
  withVar.push(() => console.log(i));
}
withVar.forEach((f) => f());

const withLet = [];
for (let j = 0; j < 3; j++) {
  withLet.push(() => console.log(j));
}
withLet.forEach((f) => f());

Output

3
3
3
0
1
2

Same loop, different keyword, different result, and being able to explain why is a common interview checkpoint.

The var closures all print 3 rather than 2, which is the detail that trips people. The loop only exits after i++ makes i equal to 3 and the condition fails, so 3 is the final value every closure sees.

None of the closures ran during the loop. All three were pushed first and called afterward, by which time the shared variable had settled.

The let version creates three separate j bindings, one per iteration, and each arrow function closes over a different one.

This is the mechanism behind the classic "setTimeout in a loop prints the last value" bug, which is the same problem with an asynchronous delay instead of a deferred call.

createPlaylist

A module with a private array and two methods over it.

function createPlaylist() {
  const songs = [];
  return {
    add(title) {
      songs.push(title);
    },
    summary() {
      return songs.length + " songs: " + songs.join(", ");
    },
  };
}

const p = createPlaylist();
p.add("Lo-fi");
p.add("Jazz");
console.log(p.summary());

Output

2 songs: Lo-fi, Jazz

The shape copies createAccount exactly, declaring the private value first and returning an object of methods that close over it.

songs is const and still grows, because push mutates the array rather than rebinding the name.

Returning songs from a getter would break the privacy, since the caller would receive the live array and could push to it directly. Returning songs.join(", ") hands out a string instead, which is a copy by nature.

add returns nothing, which is a deliberate choice. Returning songs.length would be a reasonable alternative, and returning songs would not.

The pattern generalizes to any collection that needs controlled access, and swapping the array for a Map or a Set changes nothing about the structure.