模块化怎么理解?CommonJS 和 ESM 有什么区别?
面试速答(30 秒版 TL;DR)
- 模块化解决三件事:作用域隔离、依赖管理、复用与发布。
- CommonJS(CJS)是 Node 早期的事实标准:
require/module.exports,运行时加载,导出值更像“拷贝/快照语义”(实践中容易这么理解)。 - ES Module(ESM)是语言标准:
import/export,静态结构(可在编译期分析),支持 Tree Shaking,且导出是 live binding(活绑定)。 - 面试关键点:静态 vs 动态、循环依赖行为差异、默认导出/具名导出、浏览器与 Node 的落地差异。
心智模型:静态结构带来工程能力
- ESM 静态:打包器可以在构建时知道依赖图,做 Tree Shaking、按需拆包、预加载等。
- CJS 动态:
require可以写在 if 里、函数里,依赖只有运行时才知道,优化空间更小。
CommonJS(Node 传统)
// a.cjs
module.exports = { x: 1 };
// b.cjs
const a = require("./a.cjs");
console.log(a.x);
要点:
require是同步的(经典描述:更符合服务器端文件系统语义)。module.exports是导出对象;也常写exports.foo = ...(本质是对module.exports的引用)。
ESM(语言标准)
// a.mjs
export const x = 1;
export default function f() {}
// b.mjs
import f, { x } from "./a.mjs";
要点:
import/export必须在模块顶层(语法层面静态)。- 导入的是“绑定”,不是简单值拷贝:导出方更新变量,导入方能观察到变化(live binding)。
循环依赖(面试常追)
你不需要背复杂细节,但要能说清“为什么会拿到 undefined”:
- 循环依赖下,模块会先创建导出绑定,然后按加载顺序执行;如果使用发生在赋值之前,就可能读到
undefined(或未初始化)。 - ESM 因为有 live binding,循环依赖在一些场景下比 CJS 更可控,但仍然建议避免复杂循环依赖。
工程落地差异(一句话就够)
- 浏览器原生支持 ESM(
<script type="module">)。 - Node 同时支持 CJS/ESM,但具体取决于文件扩展名与
package.json的type配置(面试提到即可,不必展开配置细节)。
易错点/坑
- 把 ESM 的
import当成“解构赋值”:不是,import { x }不是从对象上取属性。 - 混用 CJS/ESM 时的默认导出互操作:经常踩坑,工程上尽量统一模块体系。
- 循环依赖导致初始化时序问题:需要重构依赖方向。
速记要点(可背诵)
- CJS:
require,运行时加载,同步;更偏 Node 时代。 - ESM:
import/export,静态结构,可 Tree Shaking,live binding;现代标准。