completefrontend
Open lab My progress
THE JAVASCRIPT FIELD GUIDE

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.

By the end, you'll be able to
  • 01
    Spot global name clashesExplain why classic scripts share names, and why their order matters.
  • 02
    Compare module patternsTell IIFEs, namespaces, CommonJS, AMD, and ES modules apart, and say what each one costs.
  • 03
    Read modern modulesRead import and export as 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:

  • export marks a name that other files may use.
  • import brings 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.

Two lines use the same namePop out in the code editor (opens in a new tab)JavaScript
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.

Real-life analogyFrom one shared whiteboard to separate offices

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 count there
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: export says what may leave, import says what comes in
The goal

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

INTERACTIVE

A 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 var and top-level function names also become properties of the global object. In a browser, the global object is window. So var count makes window.count.
  • Top-level let, const, and class names are shared by every script too. But they don't become window properties.

The old var and the global object lesson covers this difference in more detail.

var adds a property to the global object; let does notPop out in the code editor (opens in a new tab)JavaScript
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 cart with var, so globalThis.cart exists. Line 3 prints tea.
  • Line 2 makes price with let. It is a global name too, but not a property. So line 4 prints undefined.

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.

Two scripts, one shared name
Two classic scripts choosing the same namesPop out in the code editor (opens in a new tab)HTML
<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>
Sandbox resultscart-first
cartFeaturecart:1
profileFeatureprofile:99
format()profile:99

The script that ran last decides this.

window.count99

Top-level var became a property of window.

Try it yourself

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.

The iframe has sandbox='allow-scripts' and runs fixed code. This page only accepts messages from that one iframe.
Two classic scripts. Script 1 runs first and says var count = 1. Script 2 runs last and says var count = 99. Both arrows go into one shared box called count, which ends up holding 99.script 1var count = 1script 2var count = 99runs firstruns last99countone shared name
Both scripts write to the same global 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.

Load order matters for classic scripts

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

INTERACTIVE

Before 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.

An IIFE keeps secret privatePop out in the code editor (opens in a new tab)JavaScript
(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 secret inside the function. Line 3 prints tea.
  • Code outside the function can't use the name secret.
Real-life analogyA private meeting room with one labeled tray

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, or function
In real life: One labeled tray by the shared whiteboard
In JavaScript: One namespace object, such as window.CartApp
Share one object, called a namespacePop out in the code editor (opens in a new tab)JavaScript
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 price inside 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 price through CartApp and prints 3.

The name CartApp itself is still global. If two teams both choose CartApp, they still clash.

Left: a window called inside the IIFE holds a card const price = 3, marked private. An arrow labeled return goes right to a cart called CartApp, which holds the card price: 3. CartApp is the one global name.inside the IIFEconst price = 3privatereturnprice: 3CartAppthe one global name
Inside the IIFE, price is private. The function returns one object, and only that object, CartApp, becomes a global name.
Fix it with IIFEs and namespaces
Each feature in an IIFE, sharing one objectPop out in the code editor (opens in a new tab)HTML
<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>
Results from the iframeprivate helpers
CartApp.featurecart:1
ProfileApp.featureprofile:99
window.countundefined
window.formatundefined
public namesCartApp, ProfileApp
Try it yourself

Each feature now shares just one public name. Its count and format stay private inside its own function.

This works even in very old browsers, because it only uses functions and objects. Real modules came later.

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 THROUGH

CommonJS 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.exports is 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 exports object that collects what the file shares,
  • and a cache: a store that remembers each file after its first load.
Step through a tiny require
Step 0 of 11Ready
Your turn: follow the blue line

Step through a tiny loader that works like CommonJS. Switch between the first load and the cached second load.

Running in
  1. script
Next: line 1
  "./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]));
CallStoreChangeResultBlue line: runs next. Ran: already executed.
Recent returnsNothing yet. Start with the blue line.
Choose which load to replay.

Changing this swaps the last lines of the code and starts the replay again.

A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.

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.

Two lines: first = require("./cart") runs ./cart. second = require("./cart") reads the cache. Both arrows point to the same cart called cache, holding ./cart. So first === second prints true.first = require("./cart")1st call: runs ./cartsecond = require("./cart")2nd call: reads the cache./cartcachefirst === secondprints true
The first 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.
AMD lists what it needs firstJavaScript
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.
AMD: load files without making the page wait
A cart module written with AMDJavaScript
define(["./math"], function (math) {  return {    total: function (items) {      return items.reduce(math.add, 0);    }  };});
What the loader doesmodel
  1. request ./math
  2. continue page
  3. math loaded
  4. run cart factory
  5. cart ready: 5
Step 0 of 5guess first

Guess first: what has to happen before the factory function can run?

This is a small model of how AMD loads files. It is not RequireJS itself, and it downloads nothing.

ES modules

REAL BROWSER

ES 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 export and import.

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.

A module script imports total from cart.jsHTML
<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 total from cart.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.

Left: a window called cart.js marks the function total with export. An arrow labeled import goes right to a window called module script, where total([2, 3]) gives 5.cart.jsexporttotal()importmodule scripttotal([2, 3])5
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.

A real ES module in an iframe
The module script inside the iframeHTML
<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>
Module resultsreal browser
total([2, 3])waiting for module
this === undefined…
window.count…
window.moduleLeak…
Try it yourself

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.

The iframe is sandboxed and runs fixed code, and this page checks where each message comes from. It sends its results after the module has run.

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.
Two module facts worth remembering
  • 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, this is undefined. If you mean the page's global object, write window.

Global script, IIFE, CommonJS, AMD, or ES module?

Each pattern has a clue you can spot in the code:

  • window, or a top-level var in a script tag: a classic global script.
  • A function called right where it is written: an IIFE.
  • require: CommonJS.
  • define: AMD.
  • import or export: an ES module.

The table compares the five patterns. Each row is one pattern: how it shares code, and what it costs you.

How the five module patterns share code
PatternHow does it share code?What does it cost?
Classic globalsEvery script writes its names into one shared global scope.It is simple, but names clash easily, and the script order can cause bugs.
IIFE + namespaceHelper 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.
CommonJSrequire gives back module.exports right away, while the next line waits.It suits servers and tools. Browsers needed a bundler to use it.
AMDdefine([...], factory) waits for the listed files, then runs the factory.It loads files without making the page wait, but the code is long.
ES modulesFiles 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.

Sort: global script or IIFE?
  • 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() {} }; })();
Try it yourself
0 of 4 correct

First, tell loose global names apart from the IIFE pattern.

Choose a category for every card. You can change an answer at any time; Reset clears them all.
Sort: CommonJS, AMD, or ES module?
  • 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>
Try it yourself
0 of 6 correct

Now sort each snippet by the module system it uses.

Choose a category for every card. You can change an answer at any time; Reset clears them all.

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 math and checkout in two filesJavaScript
// 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 total from cart.js.
  • Line 8 imports it into checkout.js.
  • Line 11 exports renderCheckout, which uses total to 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.js is 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.

Which one should you use?

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 EXERCISES
Exercise 1 · Warm-upPredict the clash

What are the two printed lines?

Starter codePop out in the code editor (opens in a new tab)JavaScript
var count = 1;
function format() { return "cart:" + count; }
var count = 99;
function format() { return "profile:" + count; }
console.log(format());
console.log(globalThis.count);

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · PracticeFind the public names

    Which two names does this code add to window?

    Starter codePop out in the code editor (opens in a new tab)JavaScript
    window.CartApp = (function () {
      var count = 1;
      return { feature: "cart:" + count };
    }());
    window.ProfileApp = (function () {
      var count = 99;
      return { feature: "profile:" + count };
    }());

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticeCheck the cache

      This code copies how CommonJS caches a file. What does the comparison print?

      Starter codePop out in the code editor (opens in a new tab)JavaScript
      const cache = {};
      function requireCart() {
        if (cache.cart) return cache.cart;
        cache.cart = { loadedAt: 1 };
        return cache.cart;
      }
      console.log(requireCart() === requireCart());

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 4 · PracticeStrict, like a module

        This code turns on strict mode, the same rules every module uses. What does it print?

        Starter codePop out in the code editor (opens in a new tab)JavaScript
        "use strict";
        function moduleLike() {
          return this === undefined;
        }
        console.log(moduleLike());

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 5 · ChallengePlan a module split

          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.

            Quiz: check your understanding

            7 QUESTIONS
            Lesson quiz · 7 questionsScore: first tries count
            1. Question 1 of 7Why do classic script files clash so easily?

              Choose an answer to see the explanation.

            2. Question 2 of 7What does this code print?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              var 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.

            3. Question 3 of 7What does an IIFE mainly protect?

              Choose an answer to see the explanation.

            4. Question 4 of 7This code copies what CommonJS does on a second require of the same file. What does it print?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              const 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.

            5. Question 5 of 7Which description matches AMD?

              Choose an answer to see the explanation.

            6. Question 6 of 7What is special about ES modules in a browser?

              Choose an answer to see the explanation.

            7. Question 7 of 7This code turns on strict mode, as every module does. What does it print?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              "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 var and function names also show up on window.
            • An IIFE keeps names private inside a function. It usually shares one namespace object.
            • CommonJS uses require, which waits for the file, plus module.exports and 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 import and export.

            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).

            CompleteFrontend Clear concepts. Working examples.