DMarlon Studio

Lesson 15 · 15 of 20

The module pattern and IIFEs · JavaScript

Hide internal state in JavaScript with IIFEs and the module pattern, and see how Node wraps each file and what module.exports exports.

JavaScript · The module pattern and IIFEs in DMarlon Studio: the example's code and its output in the terminal

An IIFE (immediately invoked function expression) is a function that is defined and run in the same breath: (function () { ... })(). The outer parentheses make it an expression and the final pair calls it. Everything declared inside, like items and log, stays in its private scope.

The module pattern combines an IIFE with closures: it returns an object holding only what you want to make public (add and count), and those functions still have access to items. From outside, inventory.items is undefined: the inventory can only be changed through its interface. Before JavaScript had modules, this is how web page code was organised.

Node does something very similar with every CommonJS file: it wraps it in a function with five parameters (exports, require, module, __filename and __dirname). That is why arguments.length is 5 at the top level of the file, and why its variables never mix with those of other files.

What a file exports is `module.exports`; exports is just a shortcut that initially points to the same object. If you assign a new object to module.exports, as the example does, exports still points to the old one and becomes useless: use exports.name = ... to add and module.exports = ... to replace, never exports = .... On top of that, require() caches every module, so two calls return the very same object.

Code · main.js

// IIFE: a function that runs at once and keeps its variables private.const inventory = (function () {  const items = new Map(); // nothing outside can reach this  const log = (msg) => console.log(`[inventory] ${msg}`);  return {    add(name, qty) {      items.set(name, (items.get(name) ?? 0) + qty);      log(`+${qty} ${name}`);    },    count: (name) => items.get(name) ?? 0,  };})();inventory.add("pens", 20);inventory.add("pens", 5);console.log("pens:", inventory.count("pens"), "| inventory.items:", inventory.items);// Node wraps every CommonJS file in a function just like that one:// (function (exports, require, module, __filename, __dirname) { ... })console.log("Wrapper arguments:", arguments.length, "|", typeof require, typeof module);console.log("exports === module.exports:", exports === module.exports);// Whatever ends up in module.exports is what require() returns.module.exports = { inventory, version: "1.2.0" };console.log("Exported:", Object.keys(module.exports).join(", "));console.log("exports still points to the old object:", exports !== module.exports);// require() caches modules: a second call returns the same object.const first = require("path");const second = require("path");console.log("Cached:", first === second, "| extname:", first.extname("orders.json"));

Output · node main.js

[inventory] +20 pens
[inventory] +5 pens
pens: 25 | inventory.items: undefined
Wrapper arguments: 5 | function object
exports === module.exports: true
Exported: inventory, version
exports still points to the old object: true
Cached: true | extname: .json

Key points

  • An IIFE runs as soon as it is defined and creates a private scope.
  • The module pattern exposes an interface and hides state in a closure.
  • Node wraps every CommonJS file in a function.
  • What gets exported is module.exports; exports is only an initial reference.
  • require() caches modules.

Exercise

Add a remove(name, qty) method to the inventory that refuses to leave negative stock, and a private operation counter that can be read with an operations() method.