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 name clashesExplain why classic scripts share names, and why their order matters.
- 02Compare module patternsTell IIFEs, namespaces, CommonJS, AMD, and ES modules apart, and say what each one costs.
- 03Read modern modulesRead
importandexportas the only ways in and out of a file whose names are private.
Why modules exist
Small programs fit in one file. Bigger programs don't. A shop's website might have:
- price math for the cart,
- a checkout screen,
- checks for the forms,
- code that counts what visitors click (called analytics),
- and tests.
Each part usually lives in its own file. In old-style JavaScript, every file puts its top-level names in one shared place. So one file can change a name that another file uses, by accident.
A module is a file that keeps its names private. The file chooses what to share:
exportmarks a name that other files may use.importbrings in a name that another file exported.
Before modules were part of JavaScript, people used other tricks for the same problem. You'll meet them in this lesson: IIFEs, namespaces, CommonJS, and AMD. Today JavaScript has its own built-in modules, called ES modules.
var count = 1;
var count = 2;
console.log(count);Line 1 makes count and sets it to 1. Line 2 uses the same name again and sets it to 2. Line 3 prints 2. Now imagine lines 1 and 2 are in two different files, written by two different people. The second file changes the first file's count, and nobody notices. A module gives each file its own private count.
Imagine every team in a company writes its numbers on one shared whiteboard. The cart team writes count = 1. Later, the profile team writes count = 99 in the same spot. Nobody meant any harm. They just picked the same name. Now give each team its own office. Each team writes on its own board. To share something, a team leaves it at the front desk, and other teams must ask for it by name.
- In real life: One whiteboard that the whole office shares
- In JavaScript: The global scope: every classic script can write names like
countthere - In real life: A private meeting room, plus one labeled tray by the whiteboard
- In JavaScript: An IIFE plus a namespace: private helper names, one public object
- In real life: Separate offices with a front desk and a visitor list
- In JavaScript: Modules:
exportsays what may leave,importsays what comes in
With modules, you can see what each file shares. If a file doesn't export a name, other files can't use it. If a file doesn't import a name, it shouldn't depend on it.
Problems with global scope
INTERACTIVEA classic script is a normal <script> tag without type="module". You can put several on one page. They run in order, top to bottom.
The problem is that all classic scripts share one global scope. The global scope is the outermost place where names live. Every script on the page can see it. Names act in two ways there:
- Top-level
varand top-levelfunctionnames also become properties of the global object. In a browser, the global object iswindow. Sovar countmakeswindow.count. - Top-level
let,const, andclassnames are shared by every script too. But they don't becomewindowproperties.
The old var and the global object lesson covers this difference in more detail.
var cart = "tea";
let price = 3;
console.log(globalThis.cart);
console.log(globalThis.price);globalThis is a name for the global object that works everywhere. In a browser, it is window.
- Line 1 makes
cartwithvar, soglobalThis.cartexists. Line 3 printstea. - Line 2 makes
pricewithlet. It is a global name too, but not a property. So line 4 printsundefined.
The lab below runs two real classic scripts inside an iframe, which is a small page inside this page. A sandbox setting stops that small page from touching this one. Both scripts pick the names count and format. Switch the order and see which script's version is left at the end.
<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 script that ran last decides this.
Top-level var became a property of window.
The iframe ran two real classic scripts. With this order, the profile script ran last, so its count and format are the ones left. window.count is 99.
count. Script 2 runs last, so window.count ends up as 99 and the cart's 1 is gone.This bug is hard to spot because each file looks fine on its own. The cart file says, "I need a count and a format." The profile file says the same thing. The page only breaks when both files load together. The order decides which version is left.
Classic scripts normally run in the order they appear in the HTML. Two attributes change the timing:
async: each script runs as soon as it has downloaded. If one script needs another, it may run too early.defer: scripts wait until the browser has read all the HTML, then run in order. But their names are still shared, not private.
When a shared name has the wrong value, ask: "Which script wrote this name last?" In the browser's developer tools, the Network panel shows the order the scripts loaded. Then search the code for every place that writes that name. Renaming one variable may hide the bug for now. The real fix is to stop sharing the name.
IIFE and namespace patterns
INTERACTIVEBefore JavaScript had modules, programmers often wrapped each feature in an IIFE. The letters stand for "immediately invoked function expression". It is a function that is called right where it is written.
A function has its own scope: names made inside it stay inside it. So an IIFE keeps its names private. Code outside can only get what the function returns, or what it puts on a public object.
(function () {
const secret = "tea";
console.log(secret);
}());- The round bracket at the start of line 1 and the last one on line 4 wrap the whole thing. They make JavaScript read the function as an expression, a value that can be called on the spot.
- The
()on line 4 calls the function right away. - Line 2 makes
secretinside the function. Line 3 printstea. - Code outside the function can't use the name
secret.
Think of an IIFE as a small meeting room you use for setup. You spread your papers on the table there, and nobody else sees them. When you leave, you put just one folder in a labeled tray by the shared whiteboard. Other teams can use that one folder.
- In real life: Book a meeting room and use it right away
- In JavaScript: Call a function the moment you write it:
(function () { ... }()) - In real life: Notes on the room's own table
- In JavaScript: Names made inside the function with
var,let,const, orfunction - In real life: One labeled tray by the shared whiteboard
- In JavaScript: One namespace object, such as
window.CartApp
globalThis.CartApp = (function () {
const price = 3;
return { price };
}());
console.log(globalThis.CartApp.price);A namespace is one public object that holds everything a feature wants to share.
- Line 2 makes
priceinside the IIFE, so it is private. - Line 3 returns a small object.
{ price }is short for{ price: price }. - Line 1 stores that object as
globalThis.CartApp. - Line 5 reads
pricethroughCartAppand prints 3.
The name CartApp itself is still global. If two teams both choose CartApp, they still clash.
price is private. The function returns one object, and only that object, CartApp, becomes a global name.<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>Each feature now shares just one public name. Its count and format stay private inside its own function.
Namespaces were a big step forward. One public object made it clear what each feature shared: "the cart's public tools are in CartApp." But two problems were left:
- The namespace name itself is still a global name.
- You still have to put the script tags in the right order by hand.
The next tools, CommonJS and AMD, add a loader: code that finds each file and runs it when it is needed.
A namespace is still useful for a small old widget that must work without build tools. In a new app, use it only for a short time while you move code over. First move a feature behind one object. Then switch to real imports once the page can use modules.
CommonJS and AMD
STEP THROUGHCommonJS and AMD are two older module systems. You still see them in tools, libraries, and older code.
CommonJS was Node's first module system. It uses two names:
require(id)loads another file and gives back what that file shares. It is synchronous: the next line waits until the file has loaded and run.module.exportsis the object a file fills with the things it shares.
Waiting is fine on a server, where files sit on the local disk and load fast. Three parts make CommonJS work:
- a wrapper function around each file, so the file's names stay private,
- an
exportsobject that collects what the file shares, - and a cache: a store that remembers each file after its first load.
Step through a tiny loader that works like CommonJS. 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]));Step through the code above. On the first load, require runs ./cart and keeps it in the cache. Pick "Cached second load" to see the second require skip the file and give back the same object.
require runs ./cart and keeps it in the cache. The second one gets the same object from the cache, so first === second prints true.AMD stands for Asynchronous Module Definition. It was made for browsers, where files come over the network and can be slow. A popular AMD loader was called RequireJS. AMD code has the shape define(["dep"], factory):
- The array lists the files this module needs. These are its dependencies.
- The loader downloads them asynchronously: the page keeps running while they download.
- When they arrive, the loader calls the factory, a function that builds the module. It passes the dependencies in.
define(["./price"], function (price) {
console.log(price);
});Line 1 asks for ./price before the factory runs. The factory then gets it as the price parameter, and line 2 prints it. You can't run this snippet by itself: define only exists when an AMD loader, such as RequireJS, is on the page. You don't need to write AMD for a modern browser app. But you'll see it in older packages.
Don't mix up a module format with the place it runs. A bundler is a tool that joins many module files into a few files for the browser. CommonJS can run in a browser after a bundler rewrites it. But a plain browser page has no require of its own. Node and browsers also find files in different ways:
- Node looks up package names and files with its own rules.
- Browsers treat each import path as a URL (a web address), or look it up in an import map, a list that maps short names to URLs.
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
Guess first: what has to happen before the factory function can run?
ES modules
REAL BROWSERES modules are the standard module system built into JavaScript. They arrived in ES2015, and browsers and Node both support them now. An ES module follows these rules:
- It has its own top-level scope. Its names are private to the file.
- It always runs in strict mode, the safer set of JavaScript rules.
- It runs only once per URL, even if many files import it.
- It shares values only through
exportandimport.
In a browser, you mark a module with <script type="module">. Module scripts are deferred by default: they wait until the browser has read all the HTML. A classic script without defer or async makes the browser stop reading the HTML while it runs.
<script type="module">
import { total } from "./cart.js";
console.log(total([2, 3]));
</script>- Line 1 marks the script as a module.
- Line 2 imports
totalfromcart.js. - Line 3 calls
total([2, 3])and prints the result.
Anyone reading line 2 can see that this script needs cart.js. So can tools.
cart.js shares total with export. The module script asks for it by name with import, and total([2, 3]) prints 5.Imports are written at the top of a file and never change while it runs. So a bundler can see which exports nobody imports, and leave them out of the final files. This is called tree-shaking. It makes files smaller. It is a bundler feature; browsers don't need it to run modules.
<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 import map points the name cart at a fixed module. The module script imports count and total from it. count was imported, but window.count is still undefined: module names don't land on window.
Notice the change in thinking. Old scripts ask, "Which names are already on window?" Modules ask, "Which file exports the thing I need?" The next lessons build on this: named exports, default exports, re-exports, import maps, loading a module later with import(), and mixing CommonJS with ES modules.
Modules keep names private, but they don't make code safe. Imported code can still do anything the page can do. An exported object can still be changed by other files. So:
- keep secrets on a server,
- check data that comes from outside your code,
- and export only what other files need.
- If two files import the same URL, the module runs only once. Both files get the same exports, so its top-level setup code runs once.
- At the top level of an ES module,
thisisundefined. If you mean the page's global object, writewindow.
Global script, IIFE, CommonJS, AMD, or ES module?
Each pattern has a clue you can spot in the code:
window, or a top-levelvarin a script tag: a classic global script.- A function called right where it is written: an IIFE.
require: CommonJS.define: AMD.importorexport: an ES module.
The table compares the five patterns. Each row is one pattern: how it shares code, and what it costs you.
| Pattern | How does it share code? | What does it cost? |
|---|---|---|
| Classic globals | Every script writes its names into one shared global scope. | It is simple, but names clash easily, and the script order can cause bugs. |
| IIFE + namespace | Helper names stay private, and one public object is shared. | It works everywhere. But you still order the script tags by hand, and the object's name is global. |
| CommonJS | require gives back module.exports right away, while the next line waits. | It suits servers and tools. Browsers needed a bundler to use it. |
| AMD | define([...], factory) waits for the listed files, then runs the factory. | It loads files without making the page wait, but the code is long. |
| ES modules | Files share with import and export, written at the top. | It is the standard now. You need to learn how browsers find module files by URL. |
Now sort the snippets. If you miss one, read its explanation and look for the clue.
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, tell loose global names apart from the IIFE pattern.
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 each snippet by the module system it uses.
Where you’ll use this
In real front-end work, modules let you split code by job:
- One file does the cart math.
- Another file draws the checkout.
- Tests can import the math without drawing the page.
- A new teammate can see what a file needs by reading its first lines.
// cart.js// Export only the function that checkout needs.export function total(items) { return items.reduce((sum, item) => sum + item.price, 0);} // checkout.jsimport { total } from "./cart.js"; // Checkout uses total, but the math stays in cart.js.export function renderCheckout(items) { return "Total: $" + total(items);}- Line 3 exports
totalfromcart.js. - Line 8 imports it into
checkout.js. - Line 11 exports
renderCheckout, which usestotalto build the text it shows.
A few tips for real projects:
- Make a module when some code has one clear job and only a few things to share.
- Keep helper functions unexported until another file really needs them.
- Import what you need. Don't hope that a global already exists.
- In old code, find out which pattern loads and shares the files before you rename anything.
- In a browser, an import path like
./cart.jsis a URL, so the page usually needs a web server. Node has its own rules for package names and file endings. - Imports written at the top let tools see which file needs which. That helps with caching and tree-shaking. A bundler is still optional.
- Don't move a two-line helper into its own file just to have more files. Split when it makes clear which file is in charge of the code, or when other files reuse it.
- Later you'll meet
import(), which loads a module only when it is needed. Use it for big or optional features, so the first screen loads faster.
Common misconceptions
“A separate file is always a module.”
Not quite. A browser treats a script as a module only when it has type="module", or when another module imports it. Node has its own rules, based on file endings and package settings.
“IIFEs and ES modules are the same thing.”
Both keep names private. But an IIFE is just a function you call. ES modules also have a built-in loader, import and export, strict mode, and they run once per URL.
“Modules mean nothing is shared.”
Modules stop names from leaking out by accident. But if you export an object, other files can still change that object.
“CommonJS is just old ES module syntax.”
No. CommonJS loads files synchronously with require and shares through module.exports. ES modules use import and export, and they follow different loading rules.
“AMD, CommonJS, and ES modules are bundlers.”
They are module formats: ways to write and load modules. A bundler is a separate tool. It reads modules and joins them into files that load well in a browser.
In modern browser code, use ES modules first. Learn to read CommonJS and AMD so you can understand older packages, debug files that a bundler made, and see why tools have rules for mixing the formats.
Practice: decide what each file shares
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);Line 3 sets the shared count to 99. The second format replaces the first one, so format() returns profile:99. globalThis.count is that same shared count, so the second line is 99.
Which two names does this code add to 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. Each IIFE's count stays private inside its own function.
This code copies how CommonJS caches a file. What does the comparison print?
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, the one stored in cache.cart. So === gives true.
This code turns on strict mode, the same rules every module uses. What does it print?
"use strict";
function moduleLike() {
return this === undefined;
}
console.log(moduleLike());Line 1 turns on strict mode, which every module always has. moduleLike() is a plain call, so this is undefined, and the function returns true.
One classic script holds a checkout page's price math, the code that draws the page, and its analytics. Write down three modules you would split it into. For each one, pick one function it should export.
// cart.js
// Export only the function that checkout needs.
export function total(items) {
return items.reduce((sum, item) => sum + item.price, 0);
}
// checkout.js
import { total } from "./cart.js";
// Checkout uses total, but the math stays in cart.js.
export function renderCheckout(items) {
return "Total: $" + total(items);
}Here is one way, for the price math. cart.js exports only total. checkout.js imports that one function and keeps its own drawing code. Each file shares only what the other file needs.
Quiz: check your understanding
7 QUESTIONSQuestion 1 of 7Why do classic script files clash so easily?
Choose an answer to see the explanation.
Question 2 of 7What does this code print?
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 7This code copies what CommonJS does on a second
requireof the same file. What does it print?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 ES modules in a browser?
Choose an answer to see the explanation.
Question 7 of 7This code turns on strict mode, as every module does. What does it 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 one global scope. Top-level
varandfunctionnames also show up onwindow. - An IIFE keeps names private inside a function. It usually shares one namespace object.
- CommonJS uses
require, which waits for the file, plusmodule.exportsand a cache. - AMD uses
define([deps], factory)to load files without making the page wait. - ES modules keep names private, always use strict mode, run once per URL, and share through
importandexport.
Remember the one-liner.
A module is a file that keeps its names private and shares only what it exports.
Up next: import & export. You'll learn named exports, default exports, re-exports, and live bindings (imports that always show the exporting file's current value).