PageSourceSearch

https://hoist-learn.netlify.app/chunk-BPMQmIB3.js

js hoist-learn.netlify.app collected 2026-10-03 10:53:07 UTC 9,909 bytes, 70 lines download raw bytes

1import{t as e}from"./chunk-CSe1DS_v.js";var n={id:`module-pattern`,moduleId:`scope-closures`,trackId:`core-js`,title:`IIFEs & the module pattern`,blurb:`The closure trick that held the ecosystem together before ESM.`,minutes:8,level:`working`,tldr:{model:`Wrap code in a function, run it immediately, return only the public surface. The closure is the only reference to the locals, so they are genuinely private.`,gotcha:"The wrapping parentheses are load-bearing: a statement starting with `function` parses as a declaration, so bare `function(){}()` is a SyntaxError before semantics ever run."},blocks:[{kind:`prose`,md:`For roughly fifteen years JavaScript had no module system. Every script shared one global object, and two libraries picking the same variable name broke each other silently.
2
3The workaround was pure closure: wrap your code in a function, run it immediately, and return only what the outside world should see. That is the **module pattern**, and jQuery, Underscore, Backbone and most of npm's early history are built on it.
4
5It is worth learning for three reasons \u2014 you will read it in old code, it is the clearest demonstration of closures doing real work, and the interview question "how did people do private state before \`#fields\`?" has exactly this answer.`},{kind:`heading`,text:`The IIFE, and why the parentheses matter`},{kind:`prose`,md:`An **IIFE** is an Immediately Invoked Function Expression: a function that is defined and called in one go, so its scope exists for exactly one execution and never gets a name in the enclosing scope.
6
7The parentheses are not decoration. At the start of a statement, \`function\` makes the parser expect a *declaration*, and a declaration needs a name and cannot be followed by a call. Wrapping it forces expression position.`},{kind:`code`,lang:`js`,caption:`The parse, not the semantics, is what breaks`,code:`try {
8  eval('function(){}()');
9} catch (e) {
10  console.log(e.constructor.name + ': ' + e.message);
11}
12
13console.log(typeof (function () {})());   // wrapped: parses, runs
14console.log(typeof (() => 1)());          // arrows are already expressions
15console.log(void function () { return 1; }());`,output:`SyntaxError: Function statements require a function name
16undefined
17number
18undefined`},{kind:`prose`,md:'Any operator that forces expression position works — `!function(){}()`, `void function(){}()`, `+function(){}()`. The wrapping-parentheses form won because it reads as "this is a unit" rather than as a discarded operator result. Arrow functions are expressions already, so `(() => { ... })()` needs only the call parentheses.'},{kind:`heading`,text:`Private state, before private fields`},{kind:`prose`,md:`The pattern is one closure and one returned object. Locals inside the IIFE survive because the returned methods still reference them, and they are unreachable from outside because nothing else holds a reference to that scope.`},{kind:`code`,lang:`js`,caption:`Genuinely private, not private-by-convention`,code:`var Counter = (function () {
19  let count = 0;                    // private
20  function bump() { count++; }      // private
21  return {
22    inc() { bump(); return count; },
23    get value() { return count; },
24  };
25})();
26
27console.log(Counter.inc());
28console.log(Counter.inc());
29console.log(Counter.value);
30console.log(Counter.count);`,output:`1
312
322
33undefined`},{kind:`prose`,md:"Note how strong this privacy is. There is no `_count` naming convention to respect, no `Object.keys` that reveals it, no debugger path to reach it from a reference to `Counter`. Class `#fields` give the same guarantee today; this was the only way to get it before them.\n\nReturning an object of previously-declared locals is called the **revealing module pattern**, because the return statement is a readable map of the public surface."},{kind:`code`,lang:`js`,caption:`Revealing module: the return statement is the API doc`,code:`const Store = (() => {
34  const data = new Map();
35  const set = (k, v) => data.set(k, v);
36  const get = (k) => data.get(k);
37  const size = () => data.size;
38  return { set, get, size };     // data is not on this list
39})();
40
41Store.set('a', 1);
42console.log(Store.get('a'), Store.size(), Store.data);`,output:`1 1 undefined`},{kind:`heading`,text:`Loop capture: the bug the pattern was invented to fix`},{kind:`prose`,md:"Before `let`, a `var` loop counter was **one binding shared by the whole loop**. Every closure created inside the loop saw the same variable, and after the loop that variable held its final value.\n\nAn IIFE per iteration was the fix: calling a function creates a fresh parameter binding, so each closure captured its own copy."},{kind:`code`,lang:`js`,caption:`One binding, three bindings, and the modern version`,code:`var a = [];
43for (var i = 0; i < 3; i++) a.push(function () { return i; });
44
45var b = [];
46for (var j = 0; j < 3; j++) (function (k) { b.push(function () { return k; }); })(j);
47
48let c = [];
49for (let k = 0; k < 3; k++) c.push(() => k);
50
51console.log(a.map((f) => f()));
52console.log(b.map((f) => f()));
53console.log(c.map((f) => f()));`,output:`[ 3, 3, 3 ]
54[ 0, 1, 2 ]
55[ 0, 1, 2 ]`},{kind:`callout`,tone:`tip`,title:`let in a for loop is an IIFE the engine writes for you`,md:"A `let` loop head creates a **fresh binding per iteration** and copies the value forward. That is precisely what the wrapper IIFE did by hand — which is why the pattern vanished from loop code the moment `let` shipped."},{kind:`heading`,text:`The global-object dance`},{kind:`prose`,md:"Old libraries almost always start `(function (global) { ... })(this)` or `(function (window, undefined) { ... }
55)(window)`. Two reasons, both now obsolete:\n\n- In a sloppy-mode classic script, `this` at the top level *is* the global object, so passing it in gave portable access across browser, Node and web worker.\n- Passing `undefined` as an unfilled parameter guaranteed it really was undefined, back when the global `undefined` was writable in ES3.\n\nIn a module, none of that holds — module code is always strict, so `this` inside a plain call is `undefined`, and `globalThis` covers the first need properly."},{kind:`code`,lang:`js`,caption:`Run as an ES module`,code:`console.log(typeof (function () { return this; })());
56console.log(typeof globalThis);`,output:`undefined
57object`},{kind:`heading`,text:`Where ESM actually beat it`},{kind:`compare`,aLabel:`IIFE module`,bLabel:`ES module`,rows:[{label:`Scope`,a:`Function scope, created at run time`,b:`Module scope, by definition`},{label:`Exports`,a:`An object built when the IIFE runs`,b:`Static declarations, known before execution`},{label:`Bindings`,a:`Snapshot of the reference at return time`,b:`Live — the importer sees reassignments`},{label:`Tree-shaking`,a:`Impossible; members are runtime properties`,b:`Routine; unused exports are provably dead`},{label:`Dependencies`,a:`Implicit — you order script tags by hand`,b:`A real graph the runtime resolves`},{label:`Strict mode`,a:`Opt in with a directive`,b:`Always on`}]},{kind:`prose`,md:"The decisive row is **tree-shaking**. Because an IIFE assembles its export object while running, a bundler cannot prove that `Store.size` is unused — it would have to execute the program to know. ESM's `export` declarations are statically visible, so the graph can be trimmed before anything runs.\n\nThe **live bindings** row is the one that trips people in interviews. Reassigning an internal variable after the IIFE returned does not change the returned object's property; an ESM importer would have seen the new value."},{kind:`callout`,tone:`gotcha`,title:`UMD is not a fourth module system`,md:"`(function (root, factory) { ... })` blocks that sniff for `define.amd`, then `module.exports`, then fall back to a global — that is **UMD**, and it is just an IIFE choosing which registration mechanism exists. If you meet one in a dependency, you are reading a compatibility shim, not a spec feature."},{kind:`prose`,md:"The IIFE has not disappeared, it has narrowed. It is still the right tool when you need a scope but do not have a module boundary: an `async` IIFE to use `await` inside a CommonJS file, a small computed constant, an inline `<script>` or a userscript that must leave the page's globals alone."},{kind:`code`,lang:`js`,caption:`The two IIFEs still worth writing in 2026`,code:`// A scoped async entry point in a file that has no top-level await
58(async () => {
59  const v = await Promise.resolve('ready');
60  console.log(v);
61})();
62
63// A computed constant, without leaking the intermediate steps
64const TABLE = (() => {
65  const out = [];
66  for (let i = 0; i < 4; i++) out.push(i * i);
67  return out;
68})();
69console.log(TABLE);`,output:`[ 0, 1, 4, 9 ]
70ready`},{kind:`callout`,tone:`interview`,title:`How this gets asked`,md:'Usually as *"how did you get private state before class fields?"* or *"why do old libraries wrap everything in `(function(){ })()`?"*. The complete answer names three jobs at once: **avoid globals**, **create private state**, and **capture a per-iteration binding**. Adding that ESM won on *static* analysability rather than on ergonomics is the senior half.'},{kind:`takeaways`,items:["An IIFE needs the wrapping parentheses because a statement starting with `function` parses as a declaration.",`Locals inside the IIFE are genuinely private — the closure is the only reference to them.`,`The revealing module pattern returns an object of locals, and that object snapshots the references.`,"Per-iteration IIFEs existed to fix `var` loop capture; a `let` loop head now does it for you.","`(function(){ return this; })()` is `undefined` in a module and the global object in a sloppy script.",`ESM won on static structure: live bindings, a real dependency graph and tree-shaking.`,`Still reach for an IIFE for an async entry point, a computed constant, or a script with no module boundary.`]}],cards:e};export{n as MODULE_PATTERN};

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.