正在加载,请稍候…

JavaScript ES 模块完全指南:import、export 与模块模式

掌握 JavaScript ES 模块。学习命名导出与默认导出、动态导入、循环依赖、摇树优化,以及模块与 CommonJS 的对比。

JavaScript ES 模块完全指南:import、export 与模块模式

JavaScript ES 模块完全指南:import、export 与模块模式

ES 模块(ESM)是官方的 JavaScript 模块系统。深入理解它们可以避免循环依赖、摇树优化和动态加载方面的常见问题。

命名导出

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

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

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

// 也可以在末尾导出(推荐,可读性更好)
const subtract = (a, b) => a - b;
const divide = (a, b) => a / b;

export { subtract, divide };
export { subtract as sub, divide as div }; // 导出时重命名
// 导入命名导出
import { add, multiply } from './utils/math.js';
import { subtract as sub } from './utils/math.js'; // 导入时重命名
import * as MathUtils from './utils/math.js'; // 将所有导出作为命名空间导入

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

JavaScript ES 模块完全指南:import、export 与模块模式 插图

默认导出

// 每个文件一个默认导出
// components/Button.jsx
export default function Button({ children, onClick }) {
  return <button onClick={onClick}>{children}</button>;
}

// 或者先赋值再导出
class ApiClient {
  constructor(baseUrl) {
    this.baseUrl = baseUrl;
  }
  get(path) { return fetch(this.baseUrl + path); }
}

export default ApiClient;
// 导入默认导出(可以使用任意名称)
import Button from './components/Button';
import Api from './utils/ApiClient'; // 名称与导出不同——没问题!
import MyButton from './components/Button'; // 也可以

// 同时导入默认导出和命名导出
import React, { useState, useEffect } from 'react';
//     ↑ 默认导出    ↑ 命名导出

命名导出 vs 默认导出:如何选择?

// ✅ 使用命名导出:
// - 工具函数(每个文件多个导出)
// - 常量
// - 类型/接口
// - 任何受益于一致命名的内容

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

// ✅ 使用默认导出:
// - 组件(每个文件一个)
// - 代表明确“事物”的类
// - 主要入口点

export default class UserService { ... }

// ⚠️ 避免不一致地混合模式
// 选择一个约定并坚持使用

重新导出(桶文件)

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

// 重命名后重新导出
export { default as Button } from './Button.jsx';
export { default as Input } from './Input.jsx';

// 重新导出所有内容
export * from './helpers.js';
// 现在使用者可以从一个地方导入
import { add, formatDate, Button } from './utils';
// 而不是:
// import { add } from './utils/math';
// import { formatDate } from './utils/formatting';
// import Button from './utils/Button';

JavaScript ES 模块完全指南:import、export 与模块模式 插图

动态导入

// 静态导入:在解析时求值
import { heavyLibrary } from './heavy.js'; // 始终加载,即使未使用

// 动态导入:在运行时惰性加载
async function loadFeature() {
  // 仅当调用此函数时才加载
  const { heavyLibrary } = await import('./heavy.js');
  return heavyLibrary.process(data);
}

// React 惰性加载(底层使用动态导入)
const Dashboard = React.lazy(() => import('./pages/Dashboard'));
const Settings = React.lazy(() => import('./pages/Settings'));

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

// 动态导入命名导出
const { formatDate } = await import('./utils/formatting.js');

模块实时绑定

ES 模块导出的是实时绑定,而非值——这是与 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 ← 实时绑定!值已更新!

// 在 CommonJS 中(require),这仍然是 0(复制值)

ESM 与 CommonJS 对比

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

// ES 模块
import fs from 'fs';
import { readFile } from 'fs';
export { myFunction };
export const myValue = 42;
特性 CommonJS (CJS) ES 模块 (ESM)
语法 require() / module.exports import / export
求值 同步 异步
绑定 复制值 实时绑定
摇树优化 ❌ 困难 ✅ 原生支持
顶层 await ❌ 不支持 ✅ 支持
文件扩展名 .js(默认) .mjs"type":"module"
浏览器支持 ❌ 需要打包工具 ✅ 原生支持
动态 运行时 静态(主要)

JavaScript ES 模块完全指南:import、export 与模块模式 插图

循环依赖

// ⚠️ 循环依赖——常见错误来源
// a.js
import { b } from './b.js';
export const a = 'a uses: ' + b; // 此时 b 可能为 undefined!

// b.js
import { a } from './a.js';
export const b = 'b uses: ' + a; // 此时 a 可能为 undefined!

// ESM 通过实时绑定处理此问题——到代码运行时,绑定已解析
// 但初始化顺序很重要!

// ✅ 重构:提取共享依赖
// 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;

摇树优化

ES 模块支持摇树优化(死代码消除),因为导入是静态的。

// ✅ 命名导入可被摇树优化
import { formatDate } from 'date-fns';
// 打包工具:仅包含 formatDate,而非整个库

// ❌ CommonJS 无法被摇树优化
const dateFns = require('date-fns');
dateFns.formatDate(new Date()); // 整个库被打包!

// ✅ 编写可摇树优化的库
// 分别导出每个函数
export function formatDate(date) { ... }
export function addDays(date, days) { ... }
export function startOfMonth(date) { ... }

// ❌ 导出对象(难以摇树优化)
export default {
  formatDate,
  addDays,
  startOfMonth,
};

顶层 await(仅 ESM)

// 在 ES 模块中,可以在顶层使用 await
// config.js
const config = await fetch('/api/config').then(r => r.json());

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

// 这会暂停模块求值,直到 fetch 完成
// 所有从 config.js 导入的模块都会等待它完成

在 Node.js 中使用

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

// 选项 2:在 package.json 中设置 "type": "module"
// package.json
{
  "type": "module"
}
// 现在所有 .js 文件都被视为 ESM
// 使用 .cjs 扩展名表示 CommonJS 文件

// 互操作:从 ESM 导入 CommonJS
import legacyModule from './legacy.cjs'; // 默认导入有效

→ 使用 JSON 转 YAML 转换器 在 JSON 和 YAML 配置文件之间转换。