CommonJSvsESM
JavaScript 两大模块系统的深度对决 —— 从 require() 到 import, 彻底理解它们的运行原理、差异与选型策略。
什么是模块系统?
CommonJS (CJS)
Node.js 的默认模块系统CommonJS 诞生于 2009 年,最初是为服务端 JavaScript 设计的模块规范。Node.js 全面采用了这套规范,使其成为 Node 生态的基石。它的核心思想是:在运行时同步加载模块,每个文件就是一个独立的模块作用域。
ES Modules (ESM)
ECMAScript 官方标准ESM 是 ECMAScript 2015 (ES6) 正式引入的官方模块标准。它具有静态结构,这意味着模块的依赖关系在代码执行前就可以确定, 从而支持 Tree Shaking 等高级优化。现代浏览器和 Node.js (v12+) 均已原生支持。
核心差异:一图胜千言
// ── 导出 ──
const add = (a, b) => a + b;
const PI = 3.14159;
module.exports = {
add,
PI
;
// ── 导入 ──
const math = require('./math');
console.log( math.add(1, 2)); // 3
// 解构导入
const { add } = require('./math');
console.log(add(1, 2)); // 3// ── 命名导出 ──
export const add = (a, b) => a + b;
export const PI = 3.14159;
// ── 或者默认导出 ──
export default {
add,
PI
;
// ── 导入 ──
import {
add,
PI
} from './math.mjs';
console.log(add(1, 2) ); // 3
// 默认导入
import math from './math.mjs';运行原理:引擎如何加载模块?
1CommonJS 加载流程(运行时)
// Node.js 实际上把每个文件包装成了一个函数!
(function(exports, require, module, __filename, __dirname) {
// ── 你的模块代码在这里 ──
const secret = 42; // 私有变量!
module.exports = { secret };
// 所以 exports / require / module / __filename
// 并非全局变量,而是函数参数
});2ES Modules 加载流程(三阶段)
引擎解析所有 import / export 语句,构建模块依赖图(Module Graph)。此阶段不执行任何代码。
为每个模块在内存中分配空间,将 export 和 import 指向同一块内存(活绑定 / Live Bindings)。
按照依赖拓扑顺序,从「叶节点」到「根节点」依次执行模块代码,返回最终结果。
// counter.mjs — 演示 Live Binding
export let count = 0;
export function increment() {
count++;
}
// main.mjs
import { count, increment } from './counter.mjs';
console.log(count); // 0 ← 不是拷贝,是同一引用!
increment();
console.log(count); // 1 ← 值自动更新了!
// 注意:你不能在导入方直接 count = 99
// 因为 import 绑定是只读的 (read-only view)动手试一试:循环依赖
JavaScript 模块化进化史
无模块时代
所有 JS 代码共用全局作用域,通过 script 标签顺序加载,变量冲突频发。
CommonJS 诞生
Ryan Dahl 创建 Node.js,采用 CommonJS 规范。服务端 JS 终于有了模块系统。
AMD & RequireJS
浏览器端异步模块方案出现,define() / require() 在前端流行。
UMD 统一方案
Universal Module Definition 兼容 CJS + AMD + 全局变量,成为库作者的过渡选择。
ES6 Modules 标准化 🎉
ES2015 正式引入 import / export 关键字,JS 模块化终于有了语言级别的标准。
ESM 成为主流
Node.js v12+ 支持 ESM,浏览器原生支持 <script type="module">,打包器全面适配。
选型指南:到底该用哪个?
新项目 → 用 ESM
- 支持 Tree Shaking,打包体积更小
- 浏览器原生支持,无需 bundler 也能跑
- 静态结构利于 IDE 自动补全和类型检查
- 是 TC39 官方标准,未来唯一方向
- Top-level await 支持
遗留项目 → CJS 不可避免
- npm 上大量包仍是 CommonJS 格式
- Node.js 内部许多 API 仍用 CJS 导出
- 一些工具链(Jest、某些 ESLint 插件)对 ESM 支持不完善
- CommonJS 更容易理解「执行时发生了什么」
- 在 __dirname、__filename 有用的场景下原生更方便
快速决策:3 秒选模块系统
互操作:在 ESM 中使用 CJS 包
// ✅ ESM 可以直接 import CJS 包(Node.js 自动包装默认导出)
import express from 'express';
import _ from 'lodash';
// ✅ 使用 createRequire 在 ESM 中使用 require()
import { createRequire } from 'node:module';
const require = createRequire(import.meta.url);
const data = require('./legacy-config.json'); // 读 JSON 方便
// ❌ 不能在 CJS 中直接 require() 一个 .mjs 文件
// 但可以用 dynamic import():
async function loadESM() {
const { default: foo } = await import('./my-module.mjs');
}CommonJS vs ESM 速查卡
| 场景 | 代码 | 备注 |
|---|---|---|
| 默认导出 | module.exports = value | 覆盖整个 exports 对象 |
| 命名导出 | exports.name = value | 别直接 exports = ... |
| 默认导入 | const x = require('./x') | 返回整个 exports |
| 解构导入 | const { a, b } = require('./x') | 只是语法糖 |
| 动态导入 | require(variable) | ✅ 可以使用变量 |
| JSON 导入 | require('./data.json') | Node 内置支持 |
| 条件导入 | if (cond) require('./x') | ✅ 完全合法 |
一句话总结
CommonJS 是 Node.js 生态的「遗产」,运行时同步加载、拷贝值导出; ESM 是 JavaScript 的「官方标准」,编译时静态解析、活绑定导出。新项目请无条件选择 ESM,但务必理解 CJS,因为你绕不开它。