All topics
ES6+beginner

ES Modules: import and export

The native, standardized module system for splitting JavaScript code across files, replacing older ad-hoc patterns like CommonJS and IIFEs for the browser.

ES modules are the language's own module system. Each file has its own scope, and nothing is shared unless exported.

ES Modules are like a well-organized library's card catalog: each book is only ever shelved and cataloged once no matter how many patrons check its card, and if the book's contents update in place, every patron's reference reflects the update.

Key Concepts

1
    // math.js
    export const PI = 3.14;
    export function add(a, b) { return a + b; }
    export default class Calculator {}
2
    // main.js
    import Calculator, { PI, add } from './math.js';
    import * as math from './math.js';
    import { add as sum } from './math.js';
3
Named against default. Named exports are matched by name, so a typo fails at import. A default export can be renamed freely at the import site, which makes it harder to search for.
4
Most style guides prefer named exports for that reason.
5
Imports are hoisted and static. The import list is determined before any code runs, which is what lets a bundler tree-shake — removing exports nothing imports.
6
    import { a } from './m.js';   // always at the top level
    if (x) import { b } from ...  // SyntaxError
7
Dynamic import is the runtime form, and it returns a promise:
8
    const { chart } = await import('./chart.js');
9
That is how code splitting works: the module is fetched only when this line runs.
10
Bindings are live, not copies.
    // counter.js
    export let count = 0;
    export function inc() { count++; }
11
    // main.js
    import { count, inc } from './counter.js';
    inc();
    console.log(count);    // 1 -- the binding updated
12
You cannot assign to an imported binding from outside — it is read-only at the import site.
13
Modules are always strict mode, and top-level this is undefined rather than the global object.
thisundefined
14
Against CommonJS.
    ESM        import/export, static, async loading, tree-shakeable
    CommonJS   require/module.exports, dynamic, synchronous
15
    require('./m')          evaluated at the call
    import './m'            resolved before execution
16
Which is why require can be conditional and import cannot, and why the two interoperate awkwardly in Node — .mjs, "type": "module", and the rule that CommonJS cannot require an ES module synchronously.
requireimport.mjs"type": "module"
17
Circular imports resolve to partially initialised modules rather than failing. The symptom is an undefined import that is defined later, which is worth recognising before it costs an afternoon.
undefined
18
What the interviewer is probing.1. "How do ES modules differ from CommonJS?" Probing: the two systems. Stalls: "Just syntax." Moves up: ESM is statically analysable so it tree-shakes and supports top-level await; CommonJS resolves at runtime with dynamic require.
19
2. "What does a live binding mean?" Probing: the semantics. Stalls: "A copy of the value." Moves up: imports reference the exporting module's binding, so a later change there is visible to importers — unlike a CommonJS value copy.
20
3. "Default or named exports?" Probing: the argument. Stalls: "Default is cleaner." *Moves up:* named exports rename consistently and tree-shake better; a default can be imported under any name, which hurts refactoring and search.
21
4. "How do you load a module conditionally?" Probing: dynamic import. Stalls: "You cannot." Moves up: import() returns a promise, which is also how code splitting works.