跳到主要内容

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 现在都支持它,是现代工程默认方向。

关键差异总表

维度AMDCommonJSES Module
主要场景早期浏览器Node 早期 / 服务端浏览器 + Node 通用标准
加载时机异步加载依赖同步 require静态声明,执行时实例化
语法形态define / requirerequire / module.exportsimport / 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 项目里。