AMD、CommonJS、ES Module 对比:模块系统演进、差异与落地场景
面试速答(30 秒版 TL;DR)
- 模块化要解决的是:作用域隔离、依赖声明、代码复用、按需加载。
- AMD 出现在浏览器脚本时代,核心是异步依赖加载;代表是 RequireJS。
- CommonJS(CJS)是 Node 早期事实标准,核心是
require/module.exports,特点是同步加载、运行时执行。 - ES Module(ESM)是 JavaScript 官方标准,核心是
import/export,特点是静态结构、编译期可分析、live binding。 - 面试一句话:AMD 解决浏览器时代“脚本异步加载”,CJS 解决服务器端“模块组织”,ESM 则统一成语言级标准。
心智模型:三个时代的产物
- AMD 的时代背景:浏览器没有原生模块,多个
<script>加载顺序难管理,而且网络请求昂贵,所以强调“异步加载依赖”。 - CommonJS 的时代背景:Node 主要跑在服务端,本地文件读取快,模块加载可以同步完成。
- ESM 的时代背景:前后端都需要统一标准,构建工具也需要在编译期拿到完整依赖图来做优化。
三者的最小示例
AMD
// 使用 RequireJS 这类加载器时的写法
define(["./math"], function (math) {
return {
sum: math.add(1, 2),
};
});
特点:
- 依赖列表前置声明。
- 工厂函数等依赖加载完成后才执行。
- 目标场景是浏览器脚本按需异步加载。
CommonJS
// math.cjs
module.exports = {
add(a, b) {
return a + b;
},
};
// app.cjs
const math = require("./math.cjs");
console.log(math.add(1, 2));
要点:
require()在运行时执行。- 通常是同步加载。
- 更符合 Node 早期的文件系统场景。
ES Module
// math.mjs
export function add(a, b) {
return a + b;
}
// app.mjs
import { add } from "./math.mjs";
console.log(add(1, 2));
要点:
import/export语法层面固定在顶层,便于静态分析。- 导入的是绑定,不是简单值拷贝。
- 浏览器与 Node 现在都支持它,是现代工程默认方向。
关键差异总表
| 维度 | AMD | CommonJS | ES Module |
|---|---|---|---|
| 主要场景 | 早期浏览器 | Node 早期 / 服务端 | 浏览器 + Node 通用标准 |
| 加载时机 | 异步加载依赖 | 同步 require | 静态声明,执行时实例化 |
| 语法形态 | define / require | require / module.exports | import / export |
| 依赖是否可静态分析 | 一般 | 较差 | 强 |
| Tree Shaking 友好度 | 一般 | 差 | 好 |
| 导出语义 | 工厂返回值 | 导出对象 / 值 | live binding |
| 浏览器原生支持 | 否 | 否 | 是 |
为什么 ESM 工程价值最高
1. 静态结构
ESM 的依赖在语法层面是固定的:
- 不能随便把
import写进任意 if/函数里代替静态导入; - 打包器可以提前建立依赖图;
- 所以更容易做 Tree Shaking、代码分割、预加载。
2. live binding
// counter.mjs
export let count = 0;
export function inc() {
count++;
}
// app.mjs
import { count, inc } from "./counter.mjs";
console.log(count); // 0
inc();
console.log(count); // 1
面试口径:
- ESM 导入的不是“复制出来的值”,而是对导出绑定的引用。
- 这就是为什么说它是 live binding。
循环依赖怎么答
不需要背规范细节,但至少要能说清:
- 循环依赖本质是“模块 A 初始化依赖 B,B 初始化又依赖 A”。
- CommonJS 因为是运行时
require,常会拿到一个“尚未完全初始化完成的导出对象”。 - ESM 会先建立导出绑定,再执行模块体;某些场景下比 CJS 更可控,但若在初始化前读取,同样可能出问题。
面试建议:
- 少背“谁一定输出什么”,多强调“初始化时序”。
- 复杂循环依赖通常意味着模块边界设计有问题,工程上应重构依赖方向。
易错点/坑
- 把 ESM 的
import { x }理解成对象解构;它不是从模块对象上临时取值,而是导入绑定。 - 以为 CommonJS 一定“完全是值拷贝”;更准确的说法是它导出的通常是对象引用,但消费方式和 ESM 的 live binding 仍不同。
- 混用 CJS / ESM 时忽略互操作细节,尤其是默认导出和
module.exports的映射。 - 还在现代浏览器项目里手写 AMD;现在更多是历史知识点,不是主流新项目首选。
速记要点(可背诵)
- AMD:浏览器时代的异步模块方案,代表是 RequireJS。
- CJS:Node 传统模块系统,
require同步加载,运行时决定依赖。 - ESM:官方标准模块系统,静态结构、支持 Tree Shaking、导出是 live binding。
- 现代工程默认优先 ESM;AMD 主要作为演进历史知识点,CJS 主要出现在存量 Node 项目里。