skip to content

Using only closures — no class syntax and no separate module file — how do you build a single JavaScript object that exposes a public API while keeping its internal state genuinely unreachable from outside, and what makes it unreachable?

level: middleimportance: must knowfreq 58%

answer

  1. a scope nobody outside can name
  2. return only what should be public
  3. identifier lookup goes outward, never inward
  4. the body runs exactly once
  5. singleton, eager, no injection seam

basics

~20 s

Run a function immediately and return only the object you want callers to have. Everything declared inside the function stays in that function's scope, which no outside expression can name, so the returned methods are the only path to the state. That is the module pattern.

solid answer

~50 s

You wrap the implementation in an immediately invoked function and return an object literal holding just the functions you want public. The variables declared inside the wrapper are ordinary local bindings — there is no expression outside that can name them, because scope is not reachable through property access the way an object's properties are. The returned methods close over that scope, so the environment survives the call and stays live for as long as the returned object does. The result is a singleton: the body runs exactly once, at the moment of the IIFE call, and every caller shares the one instance. This is what people mean by "the module pattern", and it was the only real encapsulation JavaScript had before ES modules and private class fields. Its weaknesses are the flip side of its strengths — one instance, created eagerly at load, with no seam to inject a dependency.

code

javascript · 23 lines
javascript
const idGenerator = (function () {
  let next = 1;                      // private state
  const issued = new Set();          // private state

  function format(n) {               // private helper
    return `id-${String(n).padStart(4, '0')}`;
  }

  return Object.freeze({
    take() {
      const id = format(next++);
      issued.add(id);
      return id;
    },
    issuedCount() { return issued.size; },
  });
})();

console.log(idGenerator.take());        // 'id-0001'
console.log(idGenerator.take());        // 'id-0002'
console.log(idGenerator.issuedCount()); // 2
console.log(Object.keys(idGenerator));  // ['take', 'issuedCount']
try { next; } catch (e) { console.log(e.name); } // 'ReferenceError'

go deeper

for a junior

Be able to write the shape from memory — a wrapper called immediately, locals inside, an object literal returned — and say that anything not in the returned object is invisible to callers.

for a middle

Explain why the locals outlive the call and why no outside expression can name them: identifier lookup walks outward through the scope chain, while property access is public by definition.

for a senior

Show the production consequences: one eagerly created instance, per-instance method copies, no injection point for fakes, and the reset-for-tests method that signals you wanted a factory.

for a principal

Own the decision of when hidden state should be a process-wide singleton at all, and what the team's rule is for state that needs to be swappable, resettable, or scoped per request.

## The shape The module pattern is one idea in three lines of syntax: create a scope, put the secrets in it, hand back a value that can reach them. ```js const settings = (function () { const values = { theme: 'dark', locale: 'en' }; // private function assertKnown(key) { // private helper if (!(key in values)) throw new Error(`unknown setting: ${key}`); } return { // the public surface get(key) { assertKnown(key); return values[key]; }, set(key, value) { assertKnown(key); values[key] = value; }, keys() { return Object.keys(values); }, }; })(); settings.set('theme', 'light'); settings.get('theme'); // 'light' values; // ReferenceError: values is not defined ``` The last line is the point. `values` is not hidden by convention or by a naming rule — there is no expression anywhere outside that function body that resolves to it. Identifier resolution walks the scope chain outward from where the code is written; it never walks *inward* into a scope you are not inside. Property access, by contrast, is a public operation: anything reachable as `obj.something` is reachable by anyone holding `obj`. ## Why the state survives the call returning The wrapper function is called once and returns immediately, but its variables do not disappear. The returned methods are closures: each captures the environment in which it was created, and the engine keeps that environment alive as long as any function that references it is reachable. So `settings` — an ordinary object — transitively keeps `values` alive, while offering no way to name it. This is also why the pattern is a *singleton*. The body executes exactly once, at the moment the IIFE is evaluated, so there is exactly one `values` object for the whole program. If you want many independent instances you drop the immediate call and export the function itself, which is a factory rather than a module. ## The revealing variant A popular stylistic variation declares everything as named functions and returns a map of the ones to expose: ```js const counterStore = (function () { let total = 0; function add(n) { total += n; } function reset() { total = 0; } function report() { return total; } return { add, report }; // reset stays private })(); ``` The advantages are that the implementation reads as plain top-to-bottom code and the returned literal doubles as an at-a-glance list of the public surface. The tradeoff is that the returned object holds the function *values* captured at return time, which has consequences when anything is reassigned later. ## What the pattern gives you that conventions do not Three things are worth naming explicitly in an interview. First, the privacy is enforced by the language, not by etiquette. A leading underscore on `_count` enforces nothing — any caller can read and write it, and a serializer will happily include it. A closure variable cannot be read, written, enumerated, serialized, or monkey-patched from outside. Second, the public surface is explicit and small. Whatever is not in the returned literal is not API, so refactoring the internals cannot break a caller who never had a handle on them. Third, initialization is co-located with the state. Any setup the module needs runs in the wrapper body before the object is returned, so callers never see a half-built object. ## What it costs Every instance carries its own copies of the method functions, because each method is created fresh inside the wrapper call. For one singleton that is irrelevant; for thousands of objects built by a factory of the same shape it is real memory that a prototype-based design would share. The object is built eagerly, at the moment the IIFE runs, whether or not anything ever uses it — the immediate call is a side effect at load time. And because there is exactly one instance created at load, tests get no seam: you cannot construct a fresh one with fake dependencies, and state left over from one test leaks into the next unless the module exposes an explicit reset. When you find yourself adding a `_resetForTests()` to the public surface, that is the pattern telling you that a factory function — same closure mechanics, without the immediate call — would have fit better. ## Freezing the surface The returned object is an ordinary object, so callers can add to it or replace its methods. If you want the public surface itself to be tamper-resistant, `Object.freeze` the returned literal. That protects the *shape* of the API; the private state was never exposed in the first place and needs no such protection.

  • What changes if you return the function instead of calling it immediately?
    You get a factory rather than a module. Each call runs the body again and produces an independent set of private bindings and an independent public object, so you can create as many instances as you need and hand each one different dependencies. The module pattern is precisely that factory invoked exactly once, at load.
  • Does `Object.freeze` on the returned object protect the private state?
    No — the private state was never exposed, so it needed no protection. Freezing stops callers from adding properties to or replacing methods on the public surface, which is about API tamper-resistance. The closure variables remain writable by the module's own methods, which is exactly what you want.
  • How would you make a module built this way testable?
    Either give it an explicit reset on the public surface, or stop calling it immediately: export the wrapper as a factory and let each test build its own instance with stubbed collaborators. Reaching for a reset method is usually a sign the singleton was the wrong shape for that piece of state.

saying these in an interview costs you the question

  • Says an underscore prefix makes a property private
  • Claims the private variables are garbage collected when the IIFE returns
  • Thinks each method call re-runs the wrapper body
  • Believes the module pattern produces independent instances
  • Says the state can be reached with Object.keys or JSON.stringify

context