ES Modules
import/export semantics — live bindings, default vs named, strict mode, module graph loading, and script type=module.
- javascript
- es-modules
- import
- export
ES modules (import / export) are the language-standard module system. They are strict mode by default, file-scoped, asynchronously loaded in browsers via the module graph, and they export live bindings — not copies of values. That last point is the interview gold.
Named and default exports
// math.js
export const PI = 3.14159;
export function add(a, b) {
return a + b;
}
export default function sum(arr) {
return arr.reduce((a, b) => a + b, 0);
}
// app.js
import sum, { add, PI } from './math.js';
import * as math from './math.js';
add(1, 2);
math.PI;
You can default-export any one value. Named exports can be many. Renames:
import { add as plus } from './math.js';
export { add as sum } from './math.js'; // re-export
export * from './math.js';
Live bindings
// counter.js
export let count = 0;
export function inc() {
count += 1;
}
import { count, inc } from './counter.js';
console.log(count); // 0
inc();
console.log(count); // 1 — binding updated
// count = 2; // SyntaxError — import is read-only binding
The importer sees updates to the exported binding. Objects are still mutable by property if exported as const obj.
Browser loading
<script type="module" src="/app.js"></script>
- Modules are deferred by default (like
defer) - Specifiers usually need explicit paths/extensions in browsers:
./foo.js - Classic scripts and modules don’t share scope
// only works as a module
import { x } from './x.js';
Static structure
import declarations are hoisted and static — you cannot import inside a non-module branch with the declaration form:
// illegal
if (cond) {
import x from './x.js';
}
// legal dynamic
if (cond) {
const x = await import('./x.js');
}
Static imports let bundlers tree-shake. See dynamic import.
Circular dependencies
Modules can form cycles. Bindings may be in the temporal dead zone until initialized — calling a function before its module finishes evaluating can throw or see incomplete state. Prefer breaking cycles with dependency inversion.
CommonJS contrast (Node mental model)
| ESM | CJS |
|---|---|
import/export |
require/module.exports |
| live bindings | copied exports object snapshot-ish |
| async graph in browsers | sync require |
this at top level is undefined |
varies |
Node supports both; dual packages have pitfalls (require of ESM).
Interview answer
“ES modules use static import/export, strict mode, and live bindings so importers see updates to exported lets. Default export is one value; named exports are many. Browsers load type=module deferred as a graph. Dynamic import() is for conditional/lazy loading. Imports are read-only bindings.”
Related
import.meta and assertions
import.meta.url; // module's URL — resolve relative assets
const url = new URL('./worker.js', import.meta.url);
// JSON modules (supported in modern tooling/browsers)
// import data from './config.json' with { type: 'json' };
import.meta.env is a bundler convention (Vite), not language core. Side-effect imports (import './polyfill.js') run the module for effects only — use sparingly and keep polyfills idempotent. Tree-shaking drops unused named exports when side-effect free; mark pure packages accordingly in library package.json.
Further reading
Related guides
- AbortControllerCancel fetch and other async work with AbortController and AbortSignal — timeouts, race conditions, and cleanup when components unmount.
- Array find, some, every, includesShort-circuiting array predicates: find, findIndex, some, every, and includes — when to use each and common interview traps.
- Array map, filter, reducemap, filter, and reduce as the core transform toolkit — immutability, chaining costs, reduce patterns, and when a plain loop is clearer.
- Arrow Functions Deep DiveLexical this, no arguments object, no construct, concise bodies — when arrows help and when methods and generators need classic functions.
- Async Iteration and for await...ofAsync iterables, for await...of, and streaming data — how async iterators differ from Promise.all and when to use each.