Chapter 9.7☕ 50 min read

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.

01🎯 Debouncing & Throttling — Event Control via Closures

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 importimport * as Name gives an object with all named exports.

4. Re-export — forward exports from another module without importing them locally.

02🔗 Once — The Run-Once Pattern

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).
V8 implements live bindings as cell references. The importing module holds a pointer to the exporting module's variable cell, not a copy of the value. When the exporter updates the variable, the cell value changes, and the importer reads the new value. This is WHY ES Modules can observe mutations across module boundaries.

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.

03🧠 Memoization Deep Dive — Fibonacci & Beyond

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.
Performance tip: Use dynamic import() for any module that isn't needed on initial page load. Charts, editors, modals, settings pages — load them when the user interacts. This can reduce initial bundle size by 50%+ and dramatically improve time-to-interactive.
04🔄 Compose & Pipe — Function Orchestration

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.

05🎬 Real-World: Redux Middleware & Express Style

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>
Two-phase module loading: ES modules are parsed, compiled, and linked in a SEPARATE phase before execution. V8 builds the entire module graph first, resolves all imports, creates placeholder cells for all exports, then executes in dependency order. This two-phase approach is WHY circular dependencies don't crash — the cells exist before any code runs.

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
Course Search
Search across all chapters & stages
📖

Search the course

Type any topic — branching, stash, rebase, hooks — and jump straight to that chapter.

merge branchesgit stashundo commitrebase