JavaScript modules let you split your code into separate files so that each file has its own scope and responsibility. A module can export values (functions, variables, classes) and other modules can import them. Modern browsers support ES Modules directly using the type="module" attribute on <script>.
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Modules Demo</title>
</head>
<body>
<h1>JavaScript Modules Demo</h1>
<script type="module" src="./app.js"></script>
</body>
</html>
window.import()."lodash") to URLs.A module can have many named exports and at most one default export. Named exports must be imported using their names, while the default export can be imported with any name.
// math.js
export const PI = 3.14159;
export function area(r) {
return PI * r * r;
}
export default function sum(a, b) {
return a + b;
}
// app.js
import add, { PI, area } from "./math.js";
console.log(add(2, 3)); // 5
console.log(PI); // 3.14159
console.log(area(10)); // 314.159
ES Modules provide flexible syntax for importing exports: namespace imports, renamed imports, and imports for side effects only.
// Import everything into a namespace object
import * as utils from "./utils.js";
utils.doThing();
// Rename on import
import { connect as open, close } from "./db.js";
// Import for side effects only (no bindings)
import "./polyfills.js";
Use dynamic import to load modules on demand. It returns a promise that resolves to the module object. This is useful for code-splitting and loading heavy features only when needed.
// app.js
const btn = document.querySelector("#chart");
btn.addEventListener("click", async () => {
const { renderChart } = await import("./charts.js");
renderChart();
});
Imports are live bindings, not copies. If the exporting module updates an exported variable, all importers see the new value automatically.
// state.js
export let count = 0;
export function inc() {
count++;
}
// app.js
import { count, inc } from "./state.js";
console.log(count); // 0
inc();
console.log(count); // 1 (live binding)
Each module has its own scope and runs in strict mode automatically. Top-level variables in a module do not become properties on the global window object.
// a.js
const secret = "scoped";
console.log(window.secret); // undefined
In the browser, you can import modules inline or via src. Module scripts are deferred by default, so they load and execute after HTML parsing.
<!-- index.html -->
<script type="module">
import { hello } from "./hello.js";
hello();
</script>
<!-- Defer is implicit for module scripts; they load asynchronously. -->
Note: Local module files should be served over HTTP(S). Opening an HTML file directly from the filesystem can block module loading due to CORS/security restrictions.
Import maps let browsers resolve bare specifiers (like "lodash") to actual URLs without bundlers.
<script type="importmap">{
"imports": {
"lodash": "https://cdn.jsdelivr.net/npm/lodash-es@4.17.21/lodash.js"
}
}</script>
<script type="module">
import { chunk } from "lodash";
console.log(chunk([1, 2, 3, 4], 2));
</script>
Modules shine in medium-to-large applications where keeping related logic together is crucial. Common patterns include organizing code by feature (auth, charts, dashboard) or by technical layer (API, UI, utils).
src/
app.js
utils/
dom.js
net.js
features/
auth/
index.js
api.js
charts/
index.js
In Node.js or older browsers, you might need a bundler or transpiler. For native ES Modules support in Node, either use the .mjs extension or set "type": "module" in package.json.
// package.json
{
"name": "app",
"type": "module"
}
In the math.js / app.js example:
console.log(add(2, 3)); prints 5 because the default export sum is imported as add.console.log(PI); prints 3.14159, the named export.console.log(area(10)); prints 314.159, computing PI * r * r.In the state.js example, calling inc() updates the exported variable count. Because imports are live bindings, the second console.log(count) shows 1 instead of 0.
For dynamic imports, when the user clicks the #chart button, the browser fetches charts.js only then, reducing the initial load time of your application.
./ and ../ to avoid confusing import errors.require (CommonJS) and import (ES Modules) in the same file unless tooling explicitly supports it.greet.js module that exports a default function and a named constant. Import both into main.js and log them to the console.api.js for data fetching and ui.js for DOM rendering. Then create an app.js that imports and wires them together.