JavaScript Interview Prep
ES6+ Features

Modules

import, export, and Dynamic Loading

LinkedIn Hook

Your bundle is 1.4 MB. The landing page ships a rich text editor that 90% of visitors never open.

One line of ES6 fixes it:

const { default: Editor } = await import("./RichEditor.js");

That is dynamic import(). It returns a Promise, the bundler splits the chunk, and the editor is fetched only when the user clicks "Edit."

ES modules are the backbone of modern JavaScript — static import for tree-shaking, export default for a module's primary shape, dynamic import() for code splitting, import.meta for the module's URL, and "live bindings" that handle circular deps without deadlock.

In this lesson I go from the basics (named vs default) all the way to the module-versus-script difference, circular dependencies, and the barrel-file re-export pattern.

If you still think of require when someone says "module" — this lesson upgrades your mental model.

Read the full lesson -> [link]

#JavaScript #InterviewPrep #ES6 #Modules #CodeSplitting #Frontend #Webpack #Vite


Modules thumbnail


What You'll Learn

  • Named vs default exports, re-exports, and the barrel file pattern
  • Dynamic import() for code splitting, conditional loading, and polyfill injection
  • The real differences between module mode and script mode, plus how ES modules handle circular dependencies via live bindings

Modules as Shipping Containers

Think of JavaScript modules like shipping containers. Each container (module) has a manifest (exports) declaring what it ships out, and an order form (imports) declaring what it needs from other containers. Before ES6 modules, JavaScript had no native way to do this.

Named Exports

// math.js — named exports
export const PI = 3.14159;
export function add(a, b) { return a + b; }
export function subtract(a, b) { return a - b; }

// Alternative: export at the bottom
const PI = 3.14159;
function add(a, b) { return a + b; }
function subtract(a, b) { return a - b; }
export { PI, add, subtract };

// Renaming on export
export { add as sum, subtract as minus };

Default Exports

// User.js — default export
export default class User {
  constructor(name) {
    this.name = name;
  }
}

// Only ONE default export per module
// You can mix default and named exports
export const ROLE = "admin";

Importing

// Named imports — must match export names (or use alias)
import { add, subtract } from "./math.js";
import { add as sum } from "./math.js";

// Default import — can use any name
import User from "./User.js";
import MyUser from "./User.js"; // Same thing, different name

// Import both default and named
import User, { ROLE } from "./User.js";

// Import everything as namespace
import * as MathUtils from "./math.js";
MathUtils.add(1, 2);
MathUtils.PI;

// Side-effect import (just runs the module)
import "./polyfills.js";

Re-exports

// index.js — barrel file re-exporting from multiple modules
export { add, subtract } from "./math.js";
export { default as User } from "./User.js";
export * from "./utils.js"; // Re-export all named exports

// Re-export with rename
export { add as sum } from "./math.js";

Dynamic import() — Code Splitting

Dynamic import() returns a Promise and allows you to load modules on demand:

// Load a module conditionally
async function loadEditor() {
  if (needsRichEditor) {
    const { default: Editor } = await import("./RichEditor.js");
    return new Editor();
  }
  const { default: Editor } = await import("./BasicEditor.js");
  return new Editor();
}

// Route-based code splitting (React pattern)
const LazyComponent = React.lazy(() => import("./HeavyComponent.js"));

// Feature detection
async function loadPolyfill() {
  if (!window.IntersectionObserver) {
    await import("./intersection-observer-polyfill.js");
  }
}

// Dynamic import with destructuring
const { add, subtract } = await import("./math.js");

import.meta

// import.meta provides metadata about the current module
console.log(import.meta.url);
// "file:///path/to/current/module.js" or "https://example.com/module.js"

// Check if this module is the entry point (Node.js)
if (import.meta.url === `file://${process.argv[1]}`) {
  main(); // Only run if executed directly
}

Module vs Script Mode Differences

// Modules (type="module"):
// - Strict mode by default
// - Top-level 'this' is undefined (not window)
// - Has import/export
// - Own scope (not global)
// - Deferred by default (like defer attribute)
// - Executed only once (even if imported multiple times)
// - CORS required for cross-origin

// Script (default):
// - Sloppy mode by default
// - Top-level 'this' is window
// - No import/export
// - Shares global scope
// - Blocks parsing by default
// - Executed every time the <script> tag is encountered

Circular Dependency Handling

// a.js
import { b } from "./b.js";
export const a = "A";
console.log("a.js:", b); // "B" (if b.js has already been evaluated)

// b.js
import { a } from "./a.js";
export const b = "B";
console.log("b.js:", a); // undefined! (a.js hasn't finished evaluating yet)

// ES modules handle circular deps via "live bindings" —
// exports are references, not copies. But the order of
// evaluation matters: the first-imported module's exports
// may be undefined when accessed during the import cycle.

Modules visual 1


Common Mistakes

  • Treating default and named exports interchangeably — import X from './m' only pulls the default export; import { X } from './m' pulls a named export. Mixing them up yields undefined imports with no build error.
  • Assuming dynamic import() behaves like require() — it returns a Promise, and the resolved value is the module namespace object, not just the default export. Always destructure const { default: X } = await import(...) when you want the default.
  • Forgetting that modules execute only once per URL — if you rely on module-level side effects (registrations, singletons), re-importing doesn't re-run them. Design module side effects to be idempotent.

Interview Questions

Q: What is the difference between named and default exports?

Named exports: multiple per module, must be imported with matching names (or aliased), imported with curly braces. Default exports: one per module, can be imported with any name, imported without curly braces. A module can have both.

Q: What is dynamic import() and when would you use it?

import() is a function-like syntax that returns a Promise resolving to the module. Use it for: code splitting (load routes on demand), conditional loading (polyfills), performance optimization (lazy load heavy libraries), and any scenario where you need to load a module at runtime rather than at parse time.

Q: How does JavaScript handle circular dependencies in ES modules?

ES modules use "live bindings" — exports are references, not copies. When a circular dependency is encountered, the engine doesn't deadlock. Instead, it returns the current state of the export (which may be undefined if the exporting module hasn't finished evaluating). This is why the order of export statements matters in circular dependency scenarios.

Q: What is import.meta?

import.meta is a module-level object containing metadata about the current module. The most common property is import.meta.url — the URL of the module. In Node.js it can be used to detect whether a module is the entry point; in the browser it is used for module-relative resource URLs.

Q: Name three ways module mode differs from script mode.

Modules run in strict mode automatically, the top-level this is undefined (not window), and modules are deferred and executed only once per URL. Script mode is sloppy-by-default, this is the global object, and each <script> tag runs independently.


Quick Reference — Cheat Sheet

ES MODULES — QUICK MAP

Exports:
  export const x = 1;                -> named
  export function f() {}             -> named
  export { a, b as c };              -> named (at bottom)
  export default class X {}          -> default (ONE per module)
  export * from './other.js';        -> re-export all
  export { default as X } from '.'   -> re-export default as named

Imports:
  import { a, b } from './m.js';     -> named
  import X from './m.js';            -> default
  import X, { a } from './m.js';     -> default + named
  import * as NS from './m.js';      -> namespace object
  import './polyfills.js';           -> side-effect only
  const m = await import('./m.js');  -> dynamic, returns Promise

Module vs script:
  - strict mode always               - sloppy mode by default
  - top-level this = undefined       - top-level this = window
  - deferred by default              - blocks parsing
  - single-evaluation                - re-evaluates per tag
  - CORS required for cross-origin   - no CORS for classic scripts

Circular dep strategy:
  "Live bindings" — exports are references.
  Partial exports may be undefined during the cycle.
  Order of evaluation matters.

Previous: let, const, Block Scoping -> Patterns and Edge Cases Next: Iterators and Iterables -> The Protocol Behind for...of


This is Lesson 9.5 of the JavaScript Interview Prep Course — 14 chapters, 87 lessons.

On this page