正在加载,请稍候…

JavaScript ES Modules Complete Guide: import, export, and Module Patterns

Master ES Modules in JavaScript. Learn named vs default exports, dynamic imports, circular dependencies, tree shaking, and how modules compare to CommonJS.

JavaScript ES Modules Complete Guide: import, export, and Module Patterns

ES Modules (ESM) are the official JavaScript module system. Understanding them thoroughly prevents common bugs around circular dependencies, tree shaking, and dynamic loading.

Named Exports

// utils/math.js
export const PI = 3.14159;

export function add(a, b) {
  return a + b;
}

export function multiply(a, b) {
  return a * b;
}

// Can also export at the end (preferred for readability)
const subtract = (a, b) => a - b;
const divide = (a, b) => a / b;

export { subtract, divide };
export { subtract as sub, divide as div }; // Rename on export
// Import named exports
import { add, multiply } from './utils/math.js';
import { subtract as sub } from './utils/math.js'; // Rename on import
import * as MathUtils from './utils/math.js'; // Import everything as namespace

console.log(add(2, 3));           // 5
console.log(MathUtils.multiply(4, 5)); // 20
console.log(sub(10, 3));          // 7

Default Exports

// One default export per file
// components/Button.jsx
export default function Button({ children, onClick }) {
  return <button onClick={onClick}>{children}</button>;
}

// Or assign then export
class ApiClient {
  constructor(baseUrl) {
    this.baseUrl = baseUrl;
  }
  get(path) { return fetch(this.baseUrl + path); }
}

export default ApiClient;
// Import default (any name you want)
import Button from './components/Button';
import Api from './utils/ApiClient'; // Different name than export — OK!
import MyButton from './components/Button'; // Also fine

// Combine default and named imports
import React, { useState, useEffect } from 'react';
//     ↑ default    ↑ named exports

Named vs Default: Which to Use?

// ✅ Use NAMED exports for:
// - Utility functions (multiple exports per file)
// - Constants
// - Types/interfaces
// - Anything that benefits from consistent naming

export const formatDate = (date) => ...;
export const formatCurrency = (amount) => ...;
export const API_URL = 'https://api.example.com';

// ✅ Use DEFAULT exports for:
// - Components (one per file)
// - Classes that represent a clear "thing"
// - Main entry points

export default class UserService { ... }

// ⚠️ Avoid mixing patterns inconsistently
// Pick a convention and stick to it

Re-exporting (Barrel Files)

// utils/index.js — barrel file
export { add, multiply } from './math.js';
export { formatDate, formatCurrency } from './formatting.js';
export { validateEmail, validatePhone } from './validation.js';

// Re-export with rename
export { default as Button } from './Button.jsx';
export { default as Input } from './Input.jsx';

// Re-export everything
export * from './helpers.js';
// Now consumers can import from one place
import { add, formatDate, Button } from './utils';
// Instead of:
// import { add } from './utils/math';
// import { formatDate } from './utils/formatting';
// import Button from './utils/Button';

Dynamic Imports

// Static import: evaluated at parse time
import { heavyLibrary } from './heavy.js'; // Always loaded, even if not used

// Dynamic import: lazy-loaded at runtime
async function loadFeature() {
  // Only loads when this function is called
  const { heavyLibrary } = await import('./heavy.js');
  return heavyLibrary.process(data);
}

// React lazy loading (uses dynamic import under the hood)
const Dashboard = React.lazy(() => import('./pages/Dashboard'));
const Settings = React.lazy(() => import('./pages/Settings'));

// Conditional loading
async function loadChart() {
  const chartLib = navigator.onLine
    ? await import('./charts/online')
    : await import('./charts/offline');
    
  return chartLib.render(data);
}

// Dynamic import with named exports
const { formatDate } = await import('./utils/formatting.js');

Module Live Bindings

ES Modules export live bindings, not values — a key difference from CommonJS.

// counter.js
export let count = 0;
export function increment() { count++; }

// main.js
import { count, increment } from './counter.js';

console.log(count); // 0
increment();
console.log(count); // 1 ← Live binding! value updated!

// In CommonJS (require), this would still be 0 (copies the value)

ESM vs CommonJS

// CommonJS (Node.js traditional)
const fs = require('fs');
const { readFile } = require('fs');
module.exports = { myFunction };
module.exports.myValue = 42;

// ES Modules
import fs from 'fs';
import { readFile } from 'fs';
export { myFunction };
export const myValue = 42;
Feature CommonJS (CJS) ES Modules (ESM)
Syntax require() / module.exports import / export
Evaluation Synchronous Asynchronous
Bindings Copies values Live bindings
Tree shaking ❌ Difficult ✅ Native
Top-level await ❌ No ✅ Yes
File extension .js (default) .mjs or "type":"module"
Browser support ❌ Needs bundler ✅ Native
Dynamic Runtime Static (mostly)

Circular Dependencies

// ⚠️ Circular dependency — common source of bugs
// a.js
import { b } from './b.js';
export const a = 'a uses: ' + b; // b might be undefined here!

// b.js
import { a } from './a.js';
export const b = 'b uses: ' + a; // a might be undefined here!

// ESM handles this with live bindings — by the time code runs, bindings resolve
// But initialization order matters!

// ✅ Refactor: extract shared dependency
// shared.js
export const shared = 'shared value';

// a.js
import { shared } from './shared.js';
export const a = 'a: ' + shared;

// b.js
import { shared } from './shared.js';
export const b = 'b: ' + shared;

Tree Shaking

ES Modules enable tree shaking (dead code elimination) because imports are static.

// ✅ Named imports are tree-shakeable
import { formatDate } from 'date-fns';
// Bundler: only includes formatDate, not the whole library

// ❌ CommonJS can't be tree-shaken
const dateFns = require('date-fns');
dateFns.formatDate(new Date()); // Entire library bundled!

// ✅ Write tree-shakeable libraries
// Export each function separately
export function formatDate(date) { ... }
export function addDays(date, days) { ... }
export function startOfMonth(date) { ... }

// ❌ Export object (hard to tree-shake)
export default {
  formatDate,
  addDays,
  startOfMonth,
};

Top-Level Await (ESM Only)

// In ES Modules, you can await at the top level
// config.js
const config = await fetch('/api/config').then(r => r.json());

export const API_URL = config.apiUrl;
export const FEATURE_FLAGS = config.features;

// This pauses the module evaluation until the fetch completes
// All modules that import from config.js wait for it to finish

In Node.js

// Option 1: .mjs extension
// mymodule.mjs
import { readFile } from 'fs/promises';
export async function readConfig(path) {
  return JSON.parse(await readFile(path, 'utf8'));
}

// Option 2: "type": "module" in package.json
// package.json
{
  "type": "module"
}
// Now all .js files are treated as ESM
// Use .cjs for CommonJS files

// Interop: import CommonJS from ESM
import legacyModule from './legacy.cjs'; // Default import works

→ Convert between JSON and YAML config files with the JSON to YAML Converter.