16 minLesson 12 of 35
Modern ES6+ JavaScript
ES Modules: import & export
ES Modules: import & export
ES Modules (ESM) are JavaScript's built-in module system. They let you split code into files, share functionality between them, and avoid the global scope pollution that plagued early JavaScript. Every modern JavaScript project uses them.
Named Exports
// math.js — export multiple things
export function add(a, b) { return a + b; }
export function subtract(a, b) { return a - b; }
export function multiply(a, b) { return a * b; }
export const PI = 3.14159;
// Or export at the end (often preferred for readability)
function divide(a, b) {
if (b === 0) throw new Error("Division by zero");
return a / b;
}
const E = 2.71828;
export { divide, E };
// main.js — import specific exports
import { add, multiply, PI } from "./math.js";
console.log(add(2, 3)); // 5
console.log(multiply(4, PI)); // 12.56636
Default Exports
// user.js — one default export per file
export default class User {
constructor(name, email) {
this.name = name;
this.email = email;
}
toString() {
return `${this.name} <${this.email}>`;
}
}
// Or a function
// api.js
export default async function fetchUser(id) {
const res = await fetch(`/api/users/${id}`);
return res.json();
}
// Import default — any name you want
import User from "./user.js";
import fetchUser from "./api.js";
import getUser from "./api.js"; // same import, different name — works
const alice = new User("Alice", "alice@example.com");
Mixing Named and Default
// config.js
export const timeout = 5000;
export const retries = 3;
const config = { timeout, retries, version: "2.0" };
export default config;
// Import both
import config, { timeout, retries } from "./config.js";
Rename on Import/Export
// Rename on export
export { add as sum, multiply as product };
// Rename on import (avoids naming conflicts)
import { add as mathAdd } from "./math.js";
import { add as arrayAdd } from "./array-utils.js";
Import Everything (* as)
import * as math from "./math.js";
math.add(1, 2); // 5
math.PI; // 3.14159
math.default; // the default export (if any)
Re-exporting (Barrel Files)
A common pattern: index.js collects and re-exports from multiple files, creating a clean public API:
// utils/string.js
export function capitalize(str) { return str[0].toUpperCase() + str.slice(1); }
export function truncate(str, len) { return str.length > len ? str.slice(0, len) + "..." : str; }
// utils/array.js
export function unique(arr) { return [...new Set(arr)]; }
export function chunk(arr, size) { /* ... */ }
// utils/index.js — re-export everything
export { capitalize, truncate } from "./string.js";
export { unique, chunk } from "./array.js";
export { default as deepClone } from "./deep-clone.js";
// Now consumers import from one place
import { capitalize, unique, deepClone } from "./utils/index.js";
Dynamic Imports
Load modules on demand — great for code splitting and lazy loading:
// Static imports are evaluated at load time
import { heavyLibrary } from "./heavy.js"; // loaded immediately
// Dynamic import — returns a Promise
async function loadChart() {
const { Chart } = await import("./chart.js");
return new Chart();
}
// Conditional loading
if (userIsAdmin) {
const { AdminPanel } = await import("./admin.js");
AdminPanel.mount();
}
// In Next.js / React for code splitting
const HeavyComponent = React.lazy(() => import("./HeavyComponent"));
Module vs Script
There are two key differences when using type="module":
<!-- Script mode — global scope, synchronous -->
<script src="old.js"></script>
<!-- Module mode — module scope, deferred by default -->
<script type="module" src="main.js"></script>
In module mode:
- Top-level code is scoped to the file (no global pollution)
thisat the top level isundefined(notwindow)- Modules are deferred automatically (like
deferattribute) - Strict mode is always on
Node.js ESM
// package.json — use "type": "module" for ESM
{
"type": "module"
}
// Node.js ESM — must include file extension
import { readFile } from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
// __dirname doesn't exist in ESM — replace with:
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
Common Patterns
// Feature detection with dynamic import
const module = await import(
navigator.onLine ? "./online-module.js" : "./offline-module.js"
);
// Import JSON (Node.js 22+)
import data from "./config.json" with { type: "json" };
// Circular imports — JavaScript handles them but they can be tricky
// If A imports B and B imports A, be careful about initialization order
Next lesson: Optional Chaining & Nullish Coalescing — safe property access without crashing.
📱
Get Notes Free →Get this course's notes on Telegram!
Free cheat sheets, summaries & practice exercises