ES Modules: import/export, Live Bindings & Dynamic import()
Global scope ka zamana gaya. ES Modules se modular code likho. Live bindings se exporter ka change importer ko dikhta hai. Dynamic import se code splitting.
Named exports let you export multiple values from a single file. Each export has a specific name that importers must match exactly.
Default export is for the "main" thing a module provides — only ONE per file. Importers can name it whatever they want.
// user.js — Named exports
export const name = "Sai";
export function greet() { return "Hello " + name; }
export class User { constructor(n) { this.name = n; } }
// OR export all at once
const name = "Sai";
function greet() { return "Hello " + name; }
export { name, greet };
// app.js — Named imports
import { name, greet } from "./user.js";
import { name as userName } from "./user.js"; // Rename!
console.log(name); // "Sai"
console.log(greet()); // "Hello Sai"
// config.js — Default export
export default {
host: "localhost",
port: 3000
};
// app.js — Default import (no braces!)
import config from "./config.js"; // Any name works
console.log(config.host); // "localhost"
// Mixed: default + named
import config, { name } from "./config.js";
// Namespace: import everything
import * as User from "./user.js";
console.log(User.name); // "Sai"
User.greet(); // "Hello Sai"
// Re-export from another module
export { name } from "./user.js"; // Re-export specific
export * from "./user.js"; // Re-export all named
Key rules:
1. Named exports — multiple per file, must use exact names in import, use braces.
2. Default export — one per file, any name on import, NO braces.
3. Namespace import — import * as Name gives an object with all named exports.
4. Re-export — forward exports from another module without importing them locally.
Modules execute ONCE — if a module is imported by 10 different files, its code runs only once. The module namespace is shared (singleton behavior).
Execution order follows depth-first, post-order traversal — all dependencies execute BEFORE the module that imports them.
Example: A imports B and C. B imports D. C imports D and E. Execution order: D → E → C → B → A.
// counter.js — Live bindings in action
export let count = 0;
export function increment() {
count++; // This change is VISIBLE to importers!
}
// app.js — Importer sees LIVE updates
import { count, increment } from "./counter.js";
console.log(count); // 0
increment();
console.log(count); // 1 — LIVE! Not a copy!
// Compare with CommonJS (Node.js require)
// counter.cjs:
// let count = 0; module.exports = { count, increment: () => count++ };
// app.cjs:
// const { count, increment } = require("./counter.cjs");
// increment(); console.log(count); // 0 — COPY! Not live!
// Module execution order example:
// a.js imports b.js and c.js
// b.js imports d.js
// c.js imports d.js and e.js
// Execution order: d → e → c → b → a (post-order DFS)
// d runs first because both b and c depend on it.
// Even though d is imported twice, it runs ONCE (singleton).
Important: Default exports are NOT live — they are a one-time binding at import time. Only named exports have live bindings. This is a key difference from CommonJS where require() always creates a snapshot copy.
Static import (import { User } from "./user.js") loads the module at startup — it can't be conditional. Every statically imported module is in your initial bundle.
Dynamic import (import("./user.js")) returns a Promise and loads the module on demand. This enables code splitting — your initial bundle stays small.
// Static import — loaded at startup (can't be conditional)
// import { HeavyChart } from "./chart.js"; // Always loaded!
// Dynamic import — loaded on demand
button.addEventListener("click", async () => {
// chart.js is NOT loaded until button is clicked!
const { HeavyChart } = await import("./chart.js");
const chart = new HeavyChart(container);
chart.render();
// This is CODE SPLITTING — chart code is in a separate bundle!
});
// Conditional loading — only load polyfill if needed
async function loadPolyfill() {
if (!window.IntersectionObserver) {
const { IntersectionObserver } = await import("./polyfill.js");
window.IntersectionObserver = IntersectionObserver;
}
}
// Route-based code splitting (SPA pattern)
async function navigate(route) {
let module;
switch (route) {
case "/dashboard":
module = await import("./routes/dashboard.js");
break;
case "/settings":
module = await import("./routes/settings.js");
break;
}
module.render(); // Only the requested route is loaded!
}
// Top-level await (ES modules only)
// const config = await fetch("/api/config").then(r => r.json());
// This module waits for the fetch before any importer can use it.
Circular dependency means A imports B, and B imports A — both depend on each other. This creates a cycle in the module graph.
What happens: A starts executing → encounters import B → B starts executing → encounters import A → A is ALREADY loading → B gets A's PARTIALLY initialized exports.
B only sees the exports that were defined BEFORE A's import of B. Later exports are undefined.
This is NOT an error — V8 handles it silently. But it can cause bugs where variables are unexpectedly undefined.
// a.js
import { b } from "./b.js";
export const a = "A";
export function getB() { return b; } // Called later — works!
// b.js
import { a } from "./a.js";
export const b = "B";
export function getA() { return a; } // Called later — works!
// What happens:
// 1. a.js starts executing
// 2. import { b } from "./b.js" — b.js starts executing
// 3. b.js encounters import { a } from "./a.js"
// 4. a.js is ALREADY loading (not finished) — b.js gets PARTIAL a.js
// 5. At this point, "a" in b.js is undefined! (not exported yet)
// 6. b.js finishes executing, exports b = "B"
// 7. a.js continues executing, exports a = "A"
// 8. NOW both are fully loaded.
// SAFE: Access circular imports INSIDE functions (called after both load)
export function getA() { return a; } // Called AFTER both modules loaded
// UNSAFE: Access circular imports at TOP LEVEL
import { a } from "./a.js";
console.log(a); // undefined! a.js hasn't finished loading yet
// FIX: Extract shared code into module C
// c.js — shared constants/functions
// a.js imports c — b.js imports c — NO cycle!
How to avoid circular dependencies:
1. Refactor — extract shared code into a third module C that both A and B import.
2. If you can't avoid — make sure the circular imports are only used AFTER both modules have fully loaded (inside functions, not at top level).
3. Linter — use ESLint's import/no-cycle rule to catch circular dependencies early.
ES modules changed how JavaScript loads and executes. Understanding the differences between <script type="module"> and classic <script> is essential.
// Classic script — global scope, blocking
// <script src="app.js"></script>
// - Runs immediately when encountered (blocks HTML parsing)
// - Variables go to window (global pollution)
// - No strict mode (unless "use strict" at top)
// - Can load from file://
// - Runs every time it is inserted
// Module script — local scope, deferred
// <script type="module" src="app.js"></script>
// - Runs AFTER HTML is parsed (like defer)
// - Variables are LOCAL to module (no global pollution)
// - Strict mode always enabled
// - CORS required (needs HTTP server, not file://)
// - Runs ONCE even if loaded multiple times
// - Top-level await is supported
// Fallback for older browsers
// <script type="module" src="app.modern.js"></script>
// <script nomodule src="app.legacy.js"></script>
// Modern browsers: load app.modern.js, ignore nomodule
// Old browsers: ignore type="module", load app.legacy.js
// Inline module
// <script type="module">
// import { greet } from "./utils.js";
// greet(); // Can import in inline module scripts!
// const secret = "hidden"; // NOT on window — local scope!
// </script>
Quick comparison:
• Classic script: sloppy mode, blocking, global scope, no CORS needed, re-executes on duplicate load
• Module script: strict mode, deferred, local scope, CORS required, runs once, supports top-level await
Lo kar liya — Key Points:
- ✅ Named exports (export const x) can have multiple per file; default export (export default x) only one per file
- ✅ Named imports use braces: import { x } from "./mod.js"; default imports don't: import x from "./mod.js"
- ✅ Modules execute ONCE (singleton) in depth-first post-order — dependencies execute before importers
- ✅ Live bindings mean importers always see the current value of named exports, not a copy
- ✅ Dynamic import() returns a Promise and loads modules on demand — enables code splitting
- ✅ Use dynamic import for: heavy modules, route-based loading, conditional polyfills
- ✅ Circular dependencies give partially-initialized exports — access circular imports inside functions, not at top level
- ✅
- ✅ Classic
Want to track your progress?
Log in to save your place and pick up where you left off.
Progress track karna chahte ho?
Login karo apni progress save karne ke liye aur jahan chhoda tha wahan se shuru karo.
Login