CommonJS 与 ES Modules

1. 核心区别总结

特性CommonJS (CJS)ES Modules (ESM)
语法require / module.exportsimport / export
加载时机运行时加载 (Runtime)编译时输出接口 (Compile-time)
输出结果值的拷贝 (浅拷贝)值的引用 (Live Binding)
this 指向当前模块 (module.exports)undefined
Tree Shaking不支持支持 (静态分析)

2. 详细解析

(1) 值的拷贝 vs 值的引用 (高频面试题)

CommonJS: 一旦输出一个值,模块内部的变化就影响不到这个值了(除非是引用类型)。

// counter.js
var count = 1;
function inc() { count++; }
module.exports = { count, inc };

// main.js
var mod = require('./counter');
mod.inc();
console.log(mod.count); // 1 (仍然是 1,因为是拷贝)

ESM: JS 引擎对脚本静态分析的时候,遇到模块加载命令 import,就会生成一个只读引用。等到脚本真正执行时,再根据这个引用,到被加载的那个模块里面去取值。

// counter.js
export let count = 1;
export function inc() { count++; }

// main.js
import { count, inc } from './counter';
inc();
console.log(count); // 2 (实时反映变化)

(2) 运行时 vs 编译时

  • CommonJS: 加载是一个对象(即 module.exports 属性),该对象只有在脚本运行完才会生成。所以无法在编译时做 Tree Shaking。
  • ESM: import 命令会被 JavaScript 引擎静态分析,优先于模块内的其他内容执行。这使得编译器能够分析出哪些功能没有被使用,从而删除死代码 (Tree Shaking)。

3. 循环依赖 (Circular Dependency)

  • CommonJS: 遇到循环加载时,只输出已经执行的部分,未执行的部分不会输出。容易导致 undefined
  • ESM: 利用由于是动态引用,只要保证使用时已经初始化即可。

4. 兼容性与互操作

在 Node.js 中:

  • .mjs 文件总是被视为 ESM。
  • .cjs 文件总是被视为 CommonJS。
  • .js 文件取决于 package.json 中的 "type": "module" 字段。

混用陷阱

  • ESM 中可以使用 import cjs from 'cjs-module' (默认导出)。
  • CommonJS 中不能直接 require ESM (因为 ESM 是异步的,CJS 是同步的),通常需要使用 await import().