一、基础对比表
| 维度 | CommonJS (CJS) | ES Module (ESM) |
|---|---|---|
| 环境 | Node.js 默认模块系统 | 浏览器原生 + Node.js(.mjs 或 type:module) |
| 语法 | require() / module.exports |
import / export |
| 加载时机 | 运行时加载 | 编译时加载(静态分析) |
| 输出方式 | 值的拷贝 | 值的只读引用(live binding) |
| 顶级 this | 指向当前模块 | undefined |
| 异步加载 | 同步(主要) | 异步(浏览器环境) |
| Tree Shaking | 不支持 | 支持(静态结构) |
| 循环依赖 | 能工作,但获取到的是部分值 | 支持,通过引用绑定 |
二、语法差异
js
// ===== CommonJS =====
// 导出
module.exports = { a: 1, b: 2 };
// 或者
exports.c = 3;
// 导入
const obj = require('./module');
const { a, b } = require('./module');
// ===== ES Module =====
// 导出
export const a = 1;
export const b = 2;
export default function() { };
export { c as d };
// 导入
import obj from './module'; // 默认导入
import { a, b } from './module'; // 具名导入
import * as all from './module'; // 命名空间导入
import obj, { a } from './module'; // 混合导入
三、核心区别详解
1. 值拷贝 vs 值引用(关键)
js
// ===== CommonJS:值拷贝 =====
// counter.js
let count = 0;
function add() { count++; }
module.exports = { count, add };
// main.js
const { count, add } = require('./counter');
console.log(count); // 0
add();
console.log(count); // 仍为 0(拷贝的是值的快照)
// ===== ES Module:实时引用 =====
// counter.mjs
export let count = 0;
export function add() { count++; }
// main.mjs
import { count, add } from './counter.mjs';
console.log(count); // 0
add();
console.log(count); // 1(实时绑定,获取最新值)
解释:
- CJS 在
require时,将导出值拷贝一份给导入方,后续变化互不影响。 - ESM 的
import是活的只读引用,导入方始终能拿到模块内部的最新值(但不能修改)。
2. 运行时加载 vs 编译时加载
js
// CommonJS:条件加载(运行时)
if (condition) {
const module = require('./moduleA'); // 可以
}
// ES Module:静态加载(编译时)
import moduleA from './moduleA'; // 只能在顶层,不能放入条件/循环
// 需要条件加载时,用动态 import()
if (condition) {
const module = await import('./moduleA.js');
}
静态结构的优势:
- 支持 Tree Shaking(打包时去除未使用代码)。
- 支持静态分析和类型推导。
- 浏览器可以提前解析并预加载依赖。
3. 循环依赖行为对比
js
// ===== CommonJS 循环依赖 =====
// a.js
exports.done = false;
const b = require('./b.js');
console.log('a.js: b.done =', b.done);
exports.done = true;
// b.js
exports.done = false;
const a = require('./a.js');
console.log('b.js: a.done =', a.done);
exports.done = true;
// main.js
require('./a.js');
// 输出:
// b.js: a.done = false (require 时 a 还没执行完,只拿到部分导出)
// a.js: b.done = true
js
// ===== ES Module 循环依赖 =====
// a.mjs
import { bDone } from './b.mjs';
console.log('a.mjs: bDone =', bDone);
export const aDone = true;
// b.mjs
import { aDone } from './a.mjs';
console.log('b.mjs: aDone =', aDone);
export const bDone = true;
// 报错或 undefined,因为 ESM 禁止在模块解析前使用未初始化绑定
// 需要小心安排引用顺序
4. 顶层 this
js
// CommonJS
console.log(this === module.exports); // true
// ES Module
console.log(this); // undefined
四、Node.js 中 ESM 与 CJS 互操作
CJS 加载 ESM
js
// Node.js 中 CJS 可以通过动态 import() 加载 ESM
const esmModule = await import('./module.mjs');
ESM 加载 CJS
js
// ESM 中可以导入 CJS 模块
import cjsModule from './module.cjs';
// 或者使用 createRequire
import { createRequire } from 'module';
const require = createRequire(import.meta.url);
const cjs = require('./module.cjs');
关键差异
- ESM 文件中不能使用
require、module、exports、__dirname、__filename(但可通过import.meta.url替代)。 - CJS 不能直接
requireESM 文件(Node.js 限制,只能通过动态import())。
五、何时使用哪种?
| 场景 | 推荐 |
|---|---|
| 新项目(前端/全栈) | ESM(标准化、Tree Shaking、浏览器原生支持) |
| NPM 库发布 | 同时提供 CJS + ESM 双格式 |
| 旧 Node.js 项目 | 保持 CJS,逐步迁移 |
| 配置/脚本 | CJS 仍方便,因为可以动态 require |
总结
核心记忆点:
- CJS 输出值的拷贝 ,ESM 输出值的实时引用。
- CJS 是运行时 加载,ESM 是编译时静态分析。
- ESM 天然支持 Tree Shaking,CJS 不行。
- 浏览器原生支持 ESM,Node.js 两者都支持。