跳到主要内容

模块化怎么理解?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.jsontype 配置(面试提到即可,不必展开配置细节)。

易错点/坑

  • 把 ESM 的 import 当成“解构赋值”:不是,import { x } 不是从对象上取属性。
  • 混用 CJS/ESM 时的默认导出互操作:经常踩坑,工程上尽量统一模块体系。
  • 循环依赖导致初始化时序问题:需要重构依赖方向。

速记要点(可背诵)

  • CJS:require,运行时加载,同步;更偏 Node 时代。
  • ESM:import/export,静态结构,可 Tree Shaking,live binding;现代标准。