Why modules?
Learn why JavaScript moved from shared global scripts to IIFEs, CommonJS, AMD, and ES modules: private scope, explicit imports and exports, safer load order, and reusable files.
- 01Spot global collisionsExplain why classic scripts share names and why order matters.
- 02Compare module patternsSeparate IIFEs, namespaces, CommonJS, AMD, and ES modules by their tradeoffs.
- 03Read modern modulesRecognize import/export as explicit file boundaries with private top-level scope.
Why modules exist
As programs grow, code stops living in one tiny file. A shopping cart has price math, a checkout screen, form validation, analytics, tests, and little helpers. If every file drops its names into the same place, the program becomes a crowded room where anyone can accidentally overwrite anyone else.
A module is JavaScript with a boundary: private names inside, and explicit doors for anything another file may use. Modern JavaScript modules use export for the front desk and import for the visitor list. Older code used IIFEs, namespaces, CommonJS, and AMD to solve parts of the same problem before ES modules became the standard.
Imagine every team in a company writing important numbers on one whiteboard. The cart team writes count = 1. The profile team arrives later and writes count = 99. Nobody broke a wall; they simply reused a shared label. Modules give each team its own office, then make sharing deliberate.
- In real life: One shared whiteboard in an open-plan office
- In JavaScript: Classic global scope: every script can write names like
count - In real life: A private meeting room with one labeled cubby
- In JavaScript: IIFE plus namespace: private helpers, one public object
- In real life: Separate offices with a front desk and visitor list
- In JavaScript: Modules:
exportsays what leaves,importsays what enters
Where the analogy stops: Offices have people and doors you can see. JavaScript modules are rules enforced by the loader and the language; exported objects can still contain mutable values if you choose to share them.
Modules make code easier to reason about because a file’s public surface is visible. If a name is not exported, other files cannot reach it directly. If a name is not imported, the file should not depend on it.
Problems with global scope
INTERACTIVEClassic browser scripts are simple: place several <script> tags on the page, and they run in order. The catch is that they share one global script scope. Top-level var and top-level function declarations become properties of the global object in browsers, so window.count and window.format appear. Top-level let, const, and class are still global lexical names, but they do not become window properties. The old var and the global object lesson digs into that distinction.
The collision lab runs real classic scripts inside a sandboxed iframe. Toggle the order and watch which feature owns the shared names.
<script>var count = 1;function format() { return "cart:" + count; }window.cartFeature = format();</script><script>var count = 99;function format() { return "profile:" + count; }window.profileFeature = format();</script><script>window.report = [cartFeature, profileFeature, format(), window.count];</script>The last script’s function wins.
Top-level var leaked to the global object.
The sandbox ran real classic scripts. With this order, profile owns the shared names, and window.count is 99.
This kind of bug is sneaky because each file can look reasonable by itself. The cart file says “I need a count and a format.” The profile file says the same thing. The page only breaks when both are loaded together, and the order decides which definition survives.
IIFE and namespace patterns
INTERACTIVEBefore standard modules, developers often wrapped a feature in an IIFE: an immediately invoked function expression. A function creates function scope, and calling it right away runs the setup. Anything declared inside stays private unless the function returns it or assigns it somewhere public.
An IIFE is like borrowing a small room for setup. You can spread papers across the table without cluttering the public whiteboard. When you leave, you put only one folder in a labeled cubby for other people to use.
- In real life: Book a meeting room
- In JavaScript: Run a function immediately:
(function () { ... }()) - In real life: Notes on the room’s private table
- In JavaScript: Local
var,let,const, and helper functions - In real life: One labeled cubby on the office whiteboard
- In JavaScript: A namespace object such as
window.CartApp
Where the analogy stops: The meeting room keeps helpers private, but the cubby label is still on the shared whiteboard. Two teams could both choose window.App, and script order is still manual.
<script>window.CartApp = (function () { var count = 1; function format() { return "cart:" + count; } return { feature: format() };}());</script><script>window.ProfileApp = (function () { var count = 99; function format() { return "profile:" + count; } return { feature: format() };}());</script>The two features still share deliberate public names, but their helper variables and functions now live inside private function scopes.
The namespace pattern was a huge improvement over loose globals. It made intent visible: “the public cart API is CartApp.” But it still relied on global namespace objects and careful script order. The next patterns add loaders.
CommonJS and AMD
STEP THROUGHCommonJS and AMD are two older module systems you still meet in tools, libraries, and historical code. CommonJS uses synchronous require(id) and module.exports. It was Node’s original module system: perfect when files are on disk and can load immediately. The important mechanics are a wrapper function, an exports object, and a cache after the first load.
Step through a tiny CommonJS-style loader. Switch between the first load and the cached second load.
script
"./math": function (module) { module.exports.add = (a, b) => a + b; }, "./cart": function (module, exports, require) { const math = require("./math"); exports.total = (items) => items.reduce(math.add, 0); }};const cache = {};function require(id) { if (cache[id]) return cache[id].exports; const module = { exports: {} }; cache[id] = module; modules[id](module, module.exports, require); return module.exports;}const cart = require("./cart");console.log(cart.total([2, 3]));AMD, often associated with RequireJS, took a different path for browsers. Its shape is define(["dep"], factory): list dependencies, load them asynchronously, then run the factory. You do not need to write AMD for modern browser apps, but recognizing it helps you read older packages.
define(["./math"], function (math) { return { total: function (items) { return items.reduce(math.add, 0); } };});- request ./math
- continue page
- math loaded
- run cart factory
- cart ready: 5
Predict what must happen before the factory can run.
ES modules
REAL BROWSERES modules are the standard JavaScript module system, standardized in ES2015 and now supported by browsers and Node. A module has its own top-level scope, is always strict, runs once per URL, and shares values only through export and import. In browsers, <script type="module"> is deferred by default, so it waits until parsing is complete instead of blocking the parser like a normal classic script.
<script type="importmap">{ "imports": { "cart": "data:text/javascript,..." } }</script><script type="module">import { count, total } from "cart";window.moduleTop = typeof count;console.log(total([2, 3]));console.log(this === undefined);</script>The module imports from a predefined data URL through an import map. Notice window.count is undefined even though count was imported and used.
Notice the shift in thinking: modules do not ask, “What names are already on window?” They ask, “Which file exports the thing I need?” That is why the next lessons can teach named exports, default exports, re-exports, import maps, dynamic import, and CommonJS interop as precise tools rather than folklore.
Global script, IIFE, CommonJS, AMD, or ES module?
The patterns have recognizable fingerprints. Sort each snippet by the system it belongs to. If you miss one, read the explanation and look for the clue: window, an immediately called function, require, define, or import/export.
| Pattern | Sharing style | Main tradeoff |
|---|---|---|
| Classic globals | Everyone writes to one shared global scope | Simple, but collisions and load-order bugs are easy |
| IIFE + namespace | Private local names, one public object | Works anywhere, but dependencies are still manual globals |
| CommonJS | require returns module.exports synchronously | Great for servers/tools; browser use historically needed bundling |
| AMD | define([...], factory) waits for dependencies | Browser-friendly async loading, but verbose |
| ES modules | Static import and export | Standard now; you must learn loader rules and file URLs |
var count = 0; function format() { return count; }window.CartApp = (function () { var count = 0; return { count }; }());- Two <script> tags both declare function init()
(() => { const secret = 1; window.App = { start() {} }; })();
First separate loose globals from the IIFE namespace workaround.
module.exports = { total }; const tax = require("./tax");define(["dep"], function (dep) { return {}; });import { total } from "./cart.js"; export { total };const cart = require("./cart"); const again = require("./cart");define(["./cart"], function (cart) { cart.start(); });<script type="module" src="app.js"></script>
Now sort the module-system snippets: CommonJS, AMD, or ES module.
Where you’ll use this
In real front-end work, modules let you split by responsibility. One file can own cart math. Another can render the checkout. Tests can import the math without rendering the page. A future teammate can see the dependency by reading the first line of the file.
// cart.jsexport function total(items) { return items.reduce((sum, item) => sum + item.price, 0);} // checkout.jsimport { total } from "./cart.js"; export function renderCheckout(items) { return "Total: $" + total(items);}- Use modules when a helper has a clear job and a small public surface.
- Keep private helpers unexported until another file genuinely needs them.
- Prefer explicit imports over “I hope this global already exists.”
- When reading legacy code, identify which pattern owns loading and sharing before changing names.
Common misconceptions
“A separate file is automatically a module.”
No. A browser treats a script as a module only when you use type="module" or when another module imports it. Node has its own package and extension rules.
“IIFEs and ES modules are the same thing.”
Both create privacy, but IIFEs are a function pattern. ES modules have a standard loader, static imports and exports, strict mode, and run-once-per-URL behavior.
“Modules mean no shared state.”
Modules stop accidental top-level leakage. If you export a mutable object, other modules can still mutate that object. Boundaries are tools, not magic.
“CommonJS is just old ES module syntax.”
CommonJS loads synchronously with require and writes to module.exports. ES modules use static syntax and different loader rules.
“AMD, CommonJS, and ES modules are bundlers.”
They are module formats or systems. Bundlers are tools that read modules and produce files optimized for delivery.
In modern browser-focused code, reach for ES modules first. Learn CommonJS and AMD so you can read existing packages, debug build output, and understand why the ecosystem has interop rules.
Practice: make the boundary visible
5 EXERCISESWhat are the two printed lines?
var count = 1;
function format() { return "cart:" + count; }
var count = 99;
function format() { return "profile:" + count; }
console.log(format());
console.log(globalThis.count);The second var count = 99 updates the shared global count, and the second function declaration makes format return profile:. The output is profile:99, then 99 from globalThis.count.
What two public names remain on window?
window.CartApp = (function () {
var count = 1;
return { feature: "cart:" + count };
}());
window.ProfileApp = (function () {
var count = 99;
return { feature: "profile:" + count };
}());Only CartApp and ProfileApp are public namespace names. Each IIFE's count is private.
Predict what the comparison prints.
const cache = {};
function requireCart() {
if (cache.cart) return cache.cart;
cache.cart = { loadedAt: 1 };
return cache.cart;
}
console.log(requireCart() === requireCart());Both calls return the same object stored at cache.cart, so strict equality is true.
What does this module-like strict-mode proof print?
"use strict";
function moduleLike() {
return this === undefined;
}
console.log(moduleLike());The function is called plainly, and strict mode makes this be undefined, so it prints true.
A checkout page has price math, DOM rendering, and analytics in one classic script. Write down three modules you would split out, then choose one function each should export.
// cart.js
export function total(items) {
return items.reduce((sum, item) => sum + item.price, 0);
}
// checkout.js
import { total } from "./cart.js";
export function renderCheckout(items) {
return "Total: $" + total(items);
}The cart file exports only total. renderCheckout imports that one public function and keeps its rendering detail separate. This is the module boundary in practice.
Quiz: check your understanding
7 QUESTIONSQuestion 1 of 7Why do classic script files collide so easily?
Choose an answer to see the explanation.
Question 2 of 7Which output shows the later classic function winning?
Read the code, then predictvar count = 1; function format() { return "cart:" + count; } var count = 99; function format() { return "profile:" + count; } console.log(format()); console.log(globalThis.count);Choose an answer to see the explanation.
Question 3 of 7What does an IIFE mainly protect?
Choose an answer to see the explanation.
Question 4 of 7What does CommonJS do on the second
requireof the same ID?Read the code, then predictconst cache = {}; function requireCart() { if (cache.cart) return cache.cart; cache.cart = { loadedAt: 1 }; return cache.cart; } console.log(requireCart() === requireCart());Choose an answer to see the explanation.
Question 5 of 7Which description matches AMD?
Choose an answer to see the explanation.
Question 6 of 7What is special about browser ES modules?
Choose an answer to see the explanation.
Question 7 of 7What does this strict-mode proof print?
Read the code, then predict"use strict"; function moduleLike() { return this === undefined; } console.log(moduleLike());Choose an answer to see the explanation.
Key takeaways
- Classic scripts share global names; top-level
varand function declarations can appear onwindow. - IIFEs create private function scope and usually expose one namespace object.
- CommonJS uses synchronous
require,module.exports, and a cache. - AMD uses
define([deps], factory)for asynchronous dependency loading. - ES modules have private top-level scope, are strict, run once per URL, and share through
import/export.
Remember the one-liner.
A module is a file with a boundary: private by default, public only through its exports.
Up next: import & export, the syntax for named exports, default exports, re-exports, and live bindings.