🧑💻 寫在開頭
點贊 + 收藏 === 學會🤣🤣🤣
面試官:你能説説 CommonJS 和 ES Module 的區別嗎?
我:……(腦子裏只剩下require和import)
説實話,這個問題你一定見過,而且99% 的前端都背過標準答案。
但真要往深了問一句:
- 為什麼 ESM 可以 Tree Shaking?CommonJS 不行
- 為什麼 ESM 的 import 是“只讀的”?
很多人,當場就開始“CPU 過載”。
於是我決定直接把底層邏輯捋清楚,以下就是我對 CommonJS 和 ES Module 一次系統性深挖的記錄。
一、什麼是 CommonJS?它解決了什麼問題?
1. CommonJS 的誕生背景
在早期 JavaScript 只有瀏覽器環境時,是沒有模塊系統的:
- 全局變量污染
- 文件之間依賴混亂
- 無法複用代碼
於是 Node.js 社區提出了一套解決方案:CommonJS(CMJ) 。
👉 注意:CommonJS 是社區標準,不是官方語言層面的規範。
CommonJS 的核心特徵
- ✅ 社區標準
- ✅ 使用函數實現(
require) - ✅ 僅 Node 環境支持
- ✅ 動態依賴,同步執行
2. CommonJS 為什麼叫“動態依賴”?
來看一段最典型的代碼:
const moduleName = './a.js'; const a = require(moduleName);
這裏的依賴路徑,是不是運行時才能確定?這就是動態依賴;
CommonJS 的依賴關係,必須等代碼執行時才能知道。
3. require 到底做了什麼?(核心原理)
你在 Node 中寫的:
const a = require('./a.js');
但如果我追問一句:require 加載的模塊代碼,是“直接執行”的嗎? 模塊裏的 this、exports、module.exports 到底從哪來的?
答案其實藏在 Node.js 對模塊的一層“函數包裝”裏:
function require(path) {
const cache = {}
// 1. 如果模塊已經加載過,直接返回緩存
if (cache[path]) {
return cache[path].exports;
}
// 2. 創建模塊對象
const module = {
id:path
exports: {}
};
// 3. 執行模塊代碼(用函數包一層)
function _run(exports, require, module, __filename, __dirname) {
// 模塊源碼在這裏執行
}
_run.call(
module.exports,
module.exports,
require,
module,
__filename,
__dirname
);
// 4. 緩存並返回結果
cache[modulePath] = module;
return module.exports;
}
假設你有一個文件 a.js,那麼文件中的內容會放到上面的_run函數中執行
我們拆開來看:
在模塊初始化階段,這三個引用的是同一個對象。
所以以下判斷永遠成立:
console.log(arguments); // [exports, require ,module, __filename, __dirname]
console.log(this); // {}
console.log(this === exports); // true
console.log(exports === module.exports); // true
重點來了:
require是一個普通函數module.exports是一個普通對象- 模塊執行是同步的
- 導出的值是一次性的值拷貝
二、ES Module:語言層面的模塊系統
如果説 CommonJS 是“工具方案”,那麼 ES Module(ESM)就是 JavaScript 官方給出的答案。
ES Module 的核心關鍵
- ✅ 官方標準(ECMAScript)
- ✅ 使用新語法(
import / export) - ✅ 所有環境支持(瀏覽器 / Node / Deno)
- ✅ 同時支持靜態依賴 & 動態依賴
- ✅ 符號綁定
1. 什麼是「靜態依賴」?
import { a } from './a.js';
這行代碼有兩個關鍵點:
import只能寫在頂層- 依賴路徑在代碼運行前就確定
👉 這意味着什麼?
- 構建工具在編譯階段就能分析依賴
- 支持Tree Shaking
- 可以做代碼分割、預加載
這也是為什麼 ESM 更適合前端工程化。
2. ESM 也支持動態依賴,但它是異步的
import('./a.js').then(module => {
console.log(module.a);
});
和 CommonJS 最大的不同點:
3. 符號綁定:ESM 最容易被忽略
這是 ESM 和 CommonJS 的本質區別。
看一段代碼
// a.js
export var a = 1;
export function changeA() {
a = 2;
}
// index.js
import { a, changeA } from './a.js';
console.log(a); // 1
changeA();
console.log(a); // 2
這裏為什麼 a 會跟着變化?
真相就是 import 不是賦值,而是“引用同一個符號”
在 ESM 中: 導入的不是值,而是對導出符號的實時綁定
可以理解為:
a在模塊內部是一個變量- 所有 import 的地方,都指向同一個 a
- 修改它,所有地方同步變化
這就是「符號綁定(Live Binding)」。
對比 CommonJS(非常關鍵)
// a.js
var n = 1;
function changeN() {
n = 2;
}
module.exports = {
n,
changeN
}
// b.js
const { n, changeN } = require('./a.js');
console.log(n); // 1
changeN();
console.log(n); // 1
這裏的 n:
- 是一次值拷貝
- 後續模塊內部怎麼改,外面都不會同步
4. 再看下 下面幾個問題
(1) export 和 export default 的區別
export:具名導出,可多個export default:默認導出,只能一個- 默認導出本質是
{ default: xxx }
(2) 下面代碼導出了什麼?
exports.a = 'a';
module.exports.b = 'b';
this.c = 'c';
module.exports = {
d: 'd'
};
{ d: 'd' }
exports.a = 1;
exports = { b: 2 };
結果:
{ a: 1 }
原因是:
exports只是module.exports的一個引用- 當你重新給
exports賦值時,只是斷開了引用關係module.exports並沒有變 - 等價於 let exports = module.exports; exports = {}; 只是改了局部變量