Introduction
A program that lives in one file is fine at 100 lines and miserable at 10,000. Names start to clash, and nobody can tell what depends on what. A module is a file with its own private scope, which chooses exactly what to share with others. This is how every serious JavaScript project is organised.
How it works
A module has its own scope. Variables declared at the top of a module are not global. Other files can use only what the module exports.
// math.js
const secret = 2; // private to this file
export function double(x) { // a named export
return x * secret;
}
export const NAME = "math tools"; // another named export
export default function square(x) { // the default export
return x * x;
}
// main.js
import square, { double, NAME } from "./math.js";
import { double as twice } from "./math.js"; // a different name here
console.log(square(3), double(4), twice(5), NAME); // 9 8 10 math tools
Named or default? A file can have many named exports, and these must be imported by their exact name, inside braces. A file can have only one default export, and the importer chooses what to call it. Many teams prefer named exports, because a wrong name is an immediate error.
Using modules on a page. <script type="module" src="main.js"></script> loads a file as a module. Modules are deferred, so the page's elements exist when they run. They are always in strict mode, and they may use await at the top level.
A module runs once. However many files import it, a module's top-level code runs one time, and every importer shares the same exported values. That is useful for a shared settings object, for example.
Bundlers. A big application can have thousands of modules. Tools called bundlers, such as Vite and esbuild, combine them into a few files that load quickly, and remove code that nothing uses.
The module pattern. Before JavaScript had modules, developers used closures to get the same privacy, and you will still meet this in older code. A function runs immediately, keeps its variables private and returns only the public parts. StepVioz runs one script at a time, so import and export cannot run in its editor. This lesson's exercises practise the idea behind them, a private inside and a small public interface, with this pattern.
Syntax
export function helper() { ... }
export default class App { ... }
import App, { helper } from "./app.js";
import * as tools from "./tools.js"; // everything, as one object
Example
// The module pattern: a private inside, and a small public interface.
const cart = (function () {
const items = [];
function add(name) {
items.push(name);
}
function count() {
return items.length;
}
return { add, count };
})();
cart.add("pen");
cart.add("book");
console.log(cart.count());
console.log(cart.items);
console.log(Object.keys(cart));Line by line
- The outer function is wrapped in brackets and called at once with
()at the end. - Inside it,
itemsis private, since nothing outside the function can name it. addandcountare closures, so they keep access toitems.- The
returngives the outside world an object with just these two functions. cart.addworks, andcountsees the result.cart.itemsisundefined, which proves that the list is hidden.
Expected result
2
undefined
[ 'add', 'count' ]
In the real world
Every npm package is a set of modules. When you write import React from "react", you are using exactly this system. Good modules have a small public interface, so that their insides can be rewritten without breaking the code that uses them.
Common mistakes
- Leaving out
./for your own files.from "math.js"looks for a package. Writefrom "./math.js". - Forgetting the braces for a named import, or adding them for a default import.
- Opening a page with modules directly from disk. Browsers block this, so serve the page from a local web server.
- Two modules importing each other, which can leave one of them seeing unfinished values. Move the shared part to a third file.
Best practices
- Give each file one clear purpose.
- Export as little as you can. Everything you export is something others may come to rely on.
- Keep modules free of side effects where possible, so that importing one simply makes its code available.
Summary
Modules give each file a private scope and share code through export and import. Named exports are imported by name, and a default export takes any name. A module runs once. The module pattern offers the same privacy using a closure.
Quick check
Key takeaways
- A module's variables are private unless they are exported.
- Named exports must be imported by name, and a file can have only one default export.
- A module's top-level code runs once, however many files import it.
Practice problems for this lesson
My notes
Write down what you want to remember from this lesson. Notes are saved on this device as you type, and appear on your dashboard.