Advanced

Modules and ES Modules

Organize code into reusable modules with import and export.

What you will see

config loaded

About 2 min of reading

Try It

Share code between files with named and default exports, then load a module on demand with dynamic import. Exported bindings stay live as the exporter changes.

1

Import two named exports and use them in one expression. The imported names must match the exported names exactly.

// math.js export const add = (a, b) => a + b; export function square(n) { return n * n; } // app.js import { add, square } from './math.js'; console.log(add(2, 3), square(4));

Before you run it, say out loud what it will print. Then open the answer.

What it prints
5 16
Named exports let you import specific pieces. Import names must match export names; rename on import with `as`.
2

Import the single default export using any name you like, since there is no required identifier to match. Instantiate it and call a method.

// logger.js export default class Logger { log(msg) { console.log(msg); } } // app.js import Logger from './logger.js'; new Logger().log("hi");

Before you run it, say out loud what it will print. Then open the answer.

What it prints
hi
A module has at most one default export. Default imports can use any name because there is no identifier to match.
3

Load a module lazily with dynamic import, which returns a promise. The module body only runs when the await resolves.

// config.js export default { theme: 'dark' }; // app.js async function loadConfig() { const config = await import('./config.js'); console.log("config loaded"); } loadConfig();

Before you run it, say out loud what it will print. Then open the answer.

What it prints
config loaded
`import()` returns a promise, so the module loads on demand. This enables code splitting and lazy loading to reduce initial bundle size.

Put it in order

Before you write anything, put these lines back in the right order. Click a line to move it up one step. This support step is easier than writing code from scratch, and it is here to make the last example stick.

Data Flow

A module exports bindings; importing modules receive live views of those bindings.
1 Module evaluates and defines exports
↓
2 Exported bindings become live
↓
3 Importer receives references
↓
4 Changes in the exporter reflect in importers
Why this works
Circular dependencies

When two modules import each other, values can be undefined at load time:

// a.js
import { b } from './b.js';
export const a = () => b();

// b.js
import { a } from './a.js';
export const b = () => a();   // may be undefined on first call

Because modules are hoisted and evaluated once, circular imports often work if you call across at runtime rather than at module top level. Design to avoid cycles when possible.

When you have a choice

Module vs script scope

Modules are strict mode by default and have their own scope. Nothing leaks to the global object:

// module.js
const secret = "hidden";
// not accessible as window.secret

// script.js
const secret = "global";   // pollutes window in classic scripts

Modules keep their variables private unless exported. This prevents name collisions and accidental global mutation, a major advantage over classic <script> tags.

Examples

Named exports and imports

Sharing code

// math.js
export const add = (a, b) => a + b;
export function square(n) { return n * n; }

// app.js
import { add, square } from './math.js';
console.log(add(2, 3), square(4));   // 5 16

Named exports let you import specific pieces. Import names must match export names exactly. You can rename on import with as.

Default exports

One primary export

// logger.js
export default class Logger {
  log(msg) { console.log(msg); }
}

// app.js
import Logger from './logger.js';   // any name works
const logger = new Logger();
logger.log("hi");

A module has at most one default export. Default imports can use any name since there is no required identifier to match.

Dynamic import

Lazy loading

async function loadConfig() {
  const config = await import('./config.js');
  return config.default;
}
// The module is fetched only when called

import() returns a Promise, so the module loads on demand. This enables code splitting and lazy loading to reduce initial bundle size.

Words this lesson uses

Each one is defined here on this site. Open the entry for the full definition and the other words it points at.

← Classes and OOP
Previous Lesson
Classes and OOP
Next Lesson
Proxy and Reflect
Proxy and Reflect →