AMD、CommonJS、ES Module:前端模块化到底是怎么演进的?
前端面试里问“说说 AMD、CommonJS、ES Module 的区别”,本质不是让你背 API,而是看你是否理解:
- 为什么早期前端需要 AMD
- 为什么 Node 选择了 CommonJS
- 为什么 ES Module 最终成为标准
- 三者在加载时机、依赖分析、运行环境、循环依赖、Tree Shaking 上到底差在哪
如果你只记一句话:
- AMD 解决的是浏览器里“脚本异步加载和依赖声明”的问题
- CommonJS 解决的是服务端里“模块同步加载和导出复用”的问题
- ES Module(ESM) 是语言标准层面的模块系统,支持静态分析,是现代前端和 Node 的主流方案
一、面试速答(30 秒版)
答:
AMD、CommonJS、ES Module 都是模块化方案,但适用场景和设计目标不同。
- AMD 主要用于早期浏览器,代表实现是 RequireJS。它把依赖前置声明,模块通过异步加载来避免阻塞页面。
- CommonJS 主要用于 Node.js,使用
require和module.exports,特点是同步加载,适合本地文件系统环境。 - ES Module 是 JavaScript 官方标准,使用
import/export。它的核心优势是静态结构,编译阶段就能分析依赖,因此支持 Tree Shaking、按需优化,也逐渐统一了浏览器和 Node 的模块体系。
如果追问区别,重点答 4 个点:
- 加载方式:AMD 异步,CommonJS 同步,ESM 支持静态导入与异步加载配合。
- 语法层级:AMD/CommonJS 是社区规范,ESM 是语言标准。
- 依赖分析时机:CommonJS 运行时,ESM 编译时。
- 导出语义:CommonJS 更像导出一个对象快照容器;ESM 是实时绑定(live binding)。
二、为什么模块化会经历 AMD → CommonJS → ESM
没有模块化时,前端代码通常是多个 <script> 直接堆在页面里:
- 全局变量互相污染
- 文件顺序强依赖,谁先加载很关键
- 依赖关系靠人脑维护
- 代码复用和拆分都很痛苦
模块化的核心目标一直没变:
- 隔离作用域
- 显式声明依赖
- 复用导出能力
- 支持更大的工程化代码组织
但不同时代的运行环境不同,所以方案也不同:
- 浏览器网络请求慢、脚本会阻塞页面,于是 AMD 强调“异步加载”
- Node 主要读本地磁盘,模块文件就在机器上,于是 CommonJS 选择“同步加载”换简单直观
- 当 JavaScript 语言本身需要标准模块能力时,就有了 ESM
演进关系可以用这张图理解:
三、AMD 是什么:为“浏览器异步加载脚本”而生
AMD 全称是 Asynchronous Module Definition,核心思想是:
- 先声明依赖
- 依赖加载完成后再执行工厂函数
- 整体围绕浏览器里的异步脚本加载设计
典型写法:
define(["./math"], function (math) {
function addTen(num) {
return math.add(num, 10);
}
return {
addTen,
};
});
3.1 AMD 的优点
- 适合浏览器环境,不会像同步脚本那样强阻塞依赖解析
- 依赖关系写在前面,可读性比“全局变量拼装”好很多
- 在打包工具普及前,确实解决了大型前端项目的模块组织问题
3.2 AMD 的问题
- 语法啰嗦,业务代码被
define/ 回调包裹 - 依赖前置虽然清晰,但写起来不自然
- 本质仍是社区方案,不是语言级能力
- 后来有了打包工具和 ESM,AMD 的必要性明显下降
3.3 AMD 适合理解成什么
面试里可以这么表述:
AMD 是早期浏览器模块化的工程化折中方案,重点不是“模块语法多优雅”,而是“如何在浏览器里异步、安全地加载依赖”。
四、CommonJS 是什么:为 Node 的同步文件系统而生
CommonJS 是 Node 早期采用的模块规范,最经典的语法是:
const math = require("./math");
function addTen(num) {
return math.add(num, 10);
}
module.exports = {
addTen,
};
4.1 CommonJS 为什么用同步加载
因为 Node 的典型运行场景是服务端:
- 模块文件通常就在本地磁盘
- 启动阶段同步读取和执行模块,成本可接受
- 同步 API 写法简单,开发体验直接
所以 require() 的特点是:
- 运行时执行
- 加载到哪里,执行到哪里
- 返回当前模块导出的对象
4.2 CommonJS 的优点
- 语法简单,容易理解
- 很符合服务端编程直觉
- Node 生态长期围绕它构建,历史包袱和兼容性都很强
4.3 CommonJS 的问题
- 同步加载 不适合浏览器直接使用
require()是运行时调用,依赖关系难以做彻底的静态分析- 对 Tree Shaking 不友好
- 在循环依赖下,拿到的可能是“尚未执行完”的部分导出
五、ES Module 为什么是终局方案
ES Module(ESM)是 JavaScript 官方标准模块系统,语法是:
// math.js
export function add(a, b) {
return a + b;
}
// index.js
import { add } from "./math.js";
console.log(add(1, 2));
它和 AMD、CommonJS 的最大差别,不只是语法更现代,而是:
模块关系是静态的,import / export 必须在顶层,编译阶段就能确定依赖图。
5.1 ESM 的核心优势
1. 静态分析友好
因为导入导出结构是固定的,构建工具在编译阶段就能知道:
- 依赖了哪些模块
- 哪些导出被实际使用
- 哪些代码可以删除
这就是 Tree Shaking 的基础。
2. 浏览器和 Node 都在统一到 ESM
现代浏览器支持原生:
<script type="module" src="./main.js"></script>
Node 也支持 ESM,只是历史上同时兼容 CommonJS,所以会有 .mjs、package.json 中 type 字段、互操作等问题。
3. 支持动态导入
静态导入之外,ESM 也支持:
const module = await import("./math.js");
这让它既保留了“静态结构”的优势,又能做懒加载和代码分割。
六、三者最核心的区别
6.1 总表对比
| 维度 | AMD | CommonJS | ES Module |
|---|---|---|---|
| 主要场景 | 早期浏览器 | Node.js / 服务端 | 浏览器 + Node + 构建工具 |
| 代表语法 | define / require | require / module.exports | import / export |
| 加载方式 | 异步 | 同步 | 静态导入为主,配合异步加载 |
| 依赖分析 | 运行前声明,但仍偏工程方案 | 运行时 | 编译时静态分析 |
| 是否语言标准 | 否 | 否 | 是 |
| Tree Shaking | 弱 | 差 | 强 |
| 浏览器原生支持 | 否 | 否 | 是 |
| 导出语义 | 工厂函数返回值 | module.exports 对象 | live binding |
6.2 一个最常被问的点:为什么 ESM 更利于 Tree Shaking
因为 Tree Shaking 的前提是:构建时能确定“哪些导出用了,哪些没用”。
CommonJS 做不到彻底静态分析,原因在于:
const mod = require(getModulePath());
这里依赖路径可能是动态计算的,构建工具很难在编译阶段完全确定。
而 ESM 通常要求:
import { foo } from "./foo.js";
依赖路径和导入成员都是静态可分析的,因此优化空间更大。
七、module.exports 和 export default 是一回事吗?
不是一回事。
这是面试里非常高频的误区。
7.1 CommonJS
module.exports = {
foo: 1,
};
本质是给当前模块导出对象赋值。
7.2 ESM
export default {
foo: 1,
};
本质是定义一个“默认导出(default export)”。
虽然很多打包工具会帮你做互转,但语义层面不同:
- CommonJS 是“导出一个对象值”
- ESM 是“导出一个模块绑定”
7.3 实战里为什么会混乱
因为 Babel、Webpack、TypeScript 等工具会做兼容层处理,例如生成:
exports.default = ...
这会让很多人误以为两者天然等价。
正确说法是:
它们可以互操作,但不是同一个规范里的同一个概念。
八、循环依赖:CommonJS 和 ESM 的表现差异
循环依赖指的是:
- A 依赖 B
- B 又依赖 A
8.1 CommonJS:拿到“执行到一半的导出”
因为 CommonJS 是运行时、同步执行模块代码,所以如果发生循环依赖,某个模块可能还没完全执行完,另一个模块就已经 require 它了。
这时拿到的是:
- 已经赋值完成的那部分导出
- 还没初始化完的部分可能是
undefined
8.2 ESM:支持 live binding,但仍要注意 TDZ 和初始化顺序
ESM 的依赖关系会先建立绑定关系,再执行模块。
因此循环依赖通常比 CommonJS 更“可控”,但并不代表你可以随便写。
如果你在变量真正初始化前访问它,仍可能触发:
- Temporal Dead Zone(暂时性死区)
ReferenceError
面试里建议这样回答:
ESM 对循环依赖的处理机制比 CommonJS 更规范,因为它先建立导入导出的绑定关系;但如果代码依赖错误的初始化时机,依然会出问题。
九、浏览器、Node、打包工具分别怎么看这三种模块
9.1 浏览器
- 早期浏览器没有原生模块机制,所以才出现 AMD
- 现代浏览器支持原生 ESM
- 但浏览器直接加载大量 ESM 文件时,请求数量、缓存策略、兼容性仍要结合工程工具处理
9.2 Node
- Node 历史包袱很重,CommonJS 生态庞大
- Node 现在同时支持 CommonJS 和 ESM
- 真实项目里经常会遇到两套模块系统混用
9.3 打包工具
Webpack、Rollup、Vite 这类工具的一个重要职责,就是:
- 解析各种模块语法
- 建依赖图
- 转成目标环境可运行的产物
为什么 Rollup 特别强调 ESM?
- 因为 ESM 的静态结构特别适合做 Tree Shaking
- 这也是现代库构建偏好 ESM 输出的重要原因
十、真实项目里怎么选
10.1 前端应用
默认优先:
- 源码层面使用 ESM
- 交给 Vite / Webpack / Rollup 处理兼容和打包
原因很简单:
- 生态主流
- 优化能力最好
- 和浏览器原生方向一致
10.2 Node 项目
如果是老项目:
- 很可能仍以 CommonJS 为主
如果是新项目:
- 可以优先考虑 ESM
- 但要确认依赖库、运行环境、测试工具链是否兼容
10.3 库开发
很多库会同时产出:
esm版本cjs版本
原因不是“重复劳动”,而是为了兼容不同消费方:
- 构建工具偏好 ESM
- 部分 Node / 老工具链仍依赖 CJS
十一、典型面试题与标准答法
Q1:AMD、CommonJS、ES Module 的区别?
答: AMD 面向浏览器异步加载,CommonJS 面向 Node 同步加载,ES Module 是 JavaScript 官方标准模块系统。AMD 和 CommonJS 都是社区规范,ESM 是语言级能力。最大的本质差异在于:CommonJS 是运行时加载,ESM 是静态结构,可在编译阶段分析依赖,因此更利于 Tree Shaking 和现代工程优化。
Q2:为什么浏览器里早期要 AMD,而不是 CommonJS?
答: 因为浏览器加载模块依赖网络请求,若像 CommonJS 那样同步加载会严重阻塞页面。AMD 的设计目标就是在浏览器环境中异步加载依赖,等依赖都就绪后再执行模块工厂函数。
Q3:为什么说 ESM 更适合 Tree Shaking?
答: 因为 ESM 的 import / export 是静态声明,构建工具编译时就能确定依赖图和导出使用情况,所以能安全删除未使用代码;而 CommonJS 的 require 是运行时调用,很多场景无法彻底静态分析。
Q4:CommonJS 和 ESM 的导出有什么语义差异?
答: CommonJS 更像导出一个对象容器,require 拿到的是模块导出结果;ESM 导出的是模块绑定,导入方拿到的是 live binding,因此导出值变化时,导入方读取到的是最新绑定结果。
Q5:现在还需要了解 AMD 吗?
答: 需要,但更多是为了理解模块化演进和维护老项目。现代前端开发实际主流已经是 ESM + 打包工具,AMD 在新项目里基本不是首选。
十二、常见误区
12.1 误区一:export default 就等于 module.exports
错。它们只是常被工具链做了兼容转换,底层语义并不相同。
12.2 误区二:ESM 一定是异步加载,CommonJS 一定是同步加载
这句话不够严谨。
- CommonJS 的
require语义是同步的 - ESM 的模块结构分析是静态的
- 至于资源文件最终怎么被加载,还和宿主环境、浏览器、打包工具有关
更准确的说法是:
ESM 是静态模块系统,不等于“天然异步执行模型”;它只是更容易和现代异步加载、代码分割结合。
12.3 误区三:用了 Webpack/Vite,就不需要理解模块规范
也错。
工具只是帮你“转译和打包”,但很多现象仍然直接来自模块语义,比如:
- 为什么 Tree Shaking 对 CJS 效果差
- 为什么循环依赖行为不同
- 为什么某些库默认导出/命名导出在互操作时会出坑
十三、速记要点
- AMD:浏览器优先,异步依赖加载,代表是 RequireJS。
- CommonJS:Node 优先,
require同步加载,module.exports导出。 - ESM:官方标准,
import/export,静态结构,支持 Tree Shaking。 - AMD 解决浏览器异步加载问题,CommonJS 解决服务端模块组织问题,ESM 解决语言标准统一和静态分析问题。
- CommonJS 偏运行时,ESM 偏编译时。
- 现代前端默认优先 ESM;理解 AMD 主要为了历史和面试,理解 CommonJS 主要为了 Node 兼容与旧生态。