以下是關于 CommonJS 和 ECMAScript Modules(ESM)的詳細對比分析,包含底層原理和示例說明:
🧩 核心差異對比表
特性 | CommonJS | ES Modules |
---|---|---|
來源 | Node.js 社區規范 | ECMAScript 語言標準 |
加載方式 | 動態加載(運行時解析) | 靜態加載(編譯時解析) |
加載環境 | Node.js 原生支持 | 瀏覽器原生支持,Node.js需開啟 --experimental-modules (v13.2+已穩定) |
語法格式 | require() / module.exports | import / export |
加載行為 | 同步加載 | 異步加載 |
模塊解析 | 文件路徑需完整 | 支持 bare module 說明符(需要導入映射) |
變量訪問 | 修改原始導出對象 | 綁定只讀引用 |
循環引用處理 | 部分加載(未完成的狀態) | 引用預解析(存在TDZ) |
頂層作用域 | 模塊內this 指向module.exports | 頂層this 為undefined |
靜態分析 | 不支持 Tree-shaking | 支持 Tree-shaking 優化 |
📦 底層加載機制差異(圖示)
CommonJS 運行時解析流程
1. 執行代碼 → 2. 構建模塊對象 → 3. 按需加載依賴 → 4. 包裹成函數執行
Module Wrapper 偽代碼:
function (exports, require, module, __filename, __dirname) {// 用戶代碼在此執行module.exports = ...;
}
ESM 預解析流程
1. 解析階段 → 2. 建立模塊關系圖 → 3. 編譯階段 → 4. 實例化 → 5. 執行代碼
關鍵特性:
- 模塊記錄(Module Record):存儲導入/導出關系
- 實時綁定(Live Bindings):導出值變化會同步到導入方
🛠? 代碼示例對比
模塊導出差異
// CommonJS 動態修改
exports.a = 1; // ? { a: 1 }
module.exports = { b: 2 }; // 最終導出 { b: 2 }// ESM 綁定不可變
export let count = 0;
export function increment() {count++; // 所有導入模塊都會看到更新后的值
}
循環依賴處理
// commonjs/a.js
console.log('a開始');
exports.done = false;
const b = require('./b'); // 此時b尚未完成加載
console.log('在a中,b.done =', b.done);
exports.done = true;
console.log('a結束');// commonjs/b.js
console.log('b開始');
exports.done = false;
const a = require('./a'); // 此時a導出{done: false}
console.log('在b中,a.done =', a.done);
exports.done = true;
console.log('b結束');
# 執行結果:
a開始 → b開始 → 在b中,a.done = false → b結束 → 在a中,b.done = true → a結束
? 現代項目中的互操作性
混合使用解決方案
// 在 ESM 中引入 CJS
import cjsModule from './commonjs-module.cjs';// 在 CJS 中引入 ESM(需異步)
const esModule = await import('./es-module.mjs');
Package.json 配置
{"type": "module", // 默認使用ESM"main": "./index.cjs", // CJS入口"exports": {"import": "./esm/index.js", // ESM入口"require": "./cjs/index.js" // CJS入口}
}
🔧 轉譯工具處理原理(以Babel為例)
# 轉換步驟示例
ESM → 解析為AST → 檢測import/export → 替換為require語法 → 添加helper函數
示例轉換效果:
// 原始ESM
import { readFile } from 'fs';
export const data = readFile('./file.txt');// 轉換后CommonJS
const { readFile } = require('fs');
exports.data = readFile('./file.txt');
🚀 性能優化差異
-
CommonJS 優化難點
- 無法預知依賴關系,阻礙并行加載
- 動態表達式導致死代碼難以消除
require(condition ? 'a' : 'b'); // 無法靜態分析
-
ESM 優化空間
// webpack利用靜態分析實現的特性 import(/* webpackPrefetch: true */ './chart'); // 預取 import(/* webpackChunkName: "utils" */ './utils'); // 分塊命名
🌐 瀏覽器支持情況
瀏覽器 | ESM支持版本 |
---|---|
Chrome | 61+ |
Firefox | 60+ |
Safari | 10.1+ |
Edge | 16+ |
<!-- 瀏覽器直接使用ESM -->
<script type="module" src="app.js"></script>
💡 選用建議
-
Node.js 服務端
- 新項目 > Node 14:優先使用ESM
- 舊項目遷移:逐步替換關鍵模塊
-
前端工程
- 統一使用ESM(配合webpack等打包工具)
- 第三方庫需提供ESM版本(通過
package.json
的module
字段)
-
工具庫開發
# 推薦雙模式發布 lib/ ├── esm/ # ESM版本(支持Tree-shaking) ├── cjs/ # CommonJS版本 └── index.d.ts # 類型聲明
兩種模塊系統在JavaScript生態中仍將長期共存,理解其底層機制有助于更高效地處理模塊化問題。隨著Node.js對ESM支持的完善,未來ESM會成為主流選擇,但CommonJS仍將在老項目中持續存在。