JavaScript中CommonJS 與 ES Modules的區(qū)別
在前端工程化的演進(jìn)長(zhǎng)河中,模塊化規(guī)范的變遷是理解 JavaScript 運(yùn)行機(jī)制的關(guān)鍵一環(huán)。對(duì)于資深開發(fā)者而言,CommonJS(簡(jiǎn)稱 CJS)與 ES Modules(簡(jiǎn)稱 ESM)不僅僅是語法的區(qū)別,更代表了 JavaScript 在服務(wù)端與瀏覽器端不同運(yùn)行環(huán)境下的架構(gòu)哲學(xué)。
本文將從底層原理出發(fā),剖析這兩大規(guī)范的核心差異,并結(jié)合 Node.js 的最新特性,探討工程化場(chǎng)景下的互操作性方案。
一、模塊化的前世今生
在 ES6 之前,JavaScript 語言層面并沒有內(nèi)置的模塊體系。這導(dǎo)致早期的大型項(xiàng)目開發(fā)極易陷入全局作用域污染、依賴關(guān)系混亂(Dependency Hell)的泥潭。為了解決這一痛點(diǎn),社區(qū)涌現(xiàn)出了多種解決方案。
CommonJS 應(yīng)運(yùn)而生,它主要面向服務(wù)器端(Node.js)。由于服務(wù)器端的文件存儲(chǔ)在本地硬盤,讀取速度極快,因此 CommonJS 采用了同步加載的設(shè)計(jì)。這一規(guī)范迅速確立了 Node.js 生態(tài)的統(tǒng)治地位。
然而,隨著前端應(yīng)用的日益復(fù)雜,瀏覽器端急需一種標(biāo)準(zhǔn)化的模塊體系。ES6(ECMAScript 2015)正式推出了 ES Modules。作為官方標(biāo)準(zhǔn),ESM 旨在統(tǒng)一瀏覽器和服務(wù)器的模塊規(guī)范,憑借其靜態(tài)編譯和異步加載的特性,逐漸成為現(xiàn)代前端構(gòu)建工具(如 Vite, Webpack, Rollup)的首選。
二、兩大規(guī)范的運(yùn)行機(jī)制與特點(diǎn)
1. CommonJS (CJS)
定位:服務(wù)器端模塊規(guī)范,Node.js 的默認(rèn)模塊系統(tǒng)。
核心特點(diǎn):
- 運(yùn)行時(shí)加載:模塊在代碼執(zhí)行階段才被加載。
- 同步加載:代碼按照編寫順序同步執(zhí)行,阻塞后續(xù)代碼直至模塊加載完成。
- 值的拷貝:導(dǎo)出的是值的副本(對(duì)于基本數(shù)據(jù)類型)。
代碼示例:
// 導(dǎo)出:module.exports
const obj = { a: 1 };
module.exports = obj;
// 引入:require
const obj = require('./test.js');
2. ES Modules (ESM)
定位:ECMAScript 官方標(biāo)準(zhǔn),旨在實(shí)現(xiàn)瀏覽器與服務(wù)端的通用。
核心特點(diǎn):
- 編譯時(shí)輸出接口:在代碼解析階段(編譯時(shí))即可確定依賴關(guān)系。
- 異步加載:支持異步加載機(jī)制,適應(yīng)網(wǎng)絡(luò)請(qǐng)求環(huán)境。
- 值的引用:導(dǎo)出的是值的動(dòng)態(tài)映射(Live Binding)。
代碼示例:
// 導(dǎo)出:export
export const obj = { name: 'ESM' };
export default { name: 'Default' };
// 引入:import
import { obj } from './test.js';
import defaultObj from './test.js';
三、深度解析——核心差異
如果要深入理解兩者的區(qū)別,必須從輸出機(jī)制、加載時(shí)機(jī)和加載方式三個(gè)維度進(jìn)行剖析。
1. 輸出值的機(jī)制:值的拷貝 vs 值的引用
這是 CJS 與 ESM 最本質(zhì)的區(qū)別,也是面試中最高頻的考察點(diǎn)。
- CommonJS:值的拷貝
CJS 模塊輸出的是一個(gè)對(duì)象,該對(duì)象在腳本運(yùn)行完后生成。一旦輸出,模塊內(nèi)部的變化就無法影響到這個(gè)值(除非導(dǎo)出的是引用類型對(duì)象且修改了其屬性,這里特指基本數(shù)據(jù)類型或引用的替換)。 - ES Modules:值的引用
ESM 模塊通過 export 導(dǎo)出的是一個(gè)靜態(tài)接口。import 導(dǎo)入的變量?jī)H僅是一個(gè)指向被導(dǎo)出模塊內(nèi)部變量的“指針”。如果模塊內(nèi)部修改了該變量,外部導(dǎo)入的地方也會(huì)感知到變化。
代碼演示:
場(chǎng)景:我們定義一個(gè) age 變量和一個(gè)自增函數(shù) addAge。
CommonJS 實(shí)現(xiàn):
// lib.js
let age = 18;
module.exports = {
age,
addAge: function () {
age++;
},
};
// main.js
const { age, addAge } = require('./lib.js');
console.log(age); // 18
addAge();
console.log(age); // 18 (注意:這里依然是 18,因?yàn)閷?dǎo)出的是 age 變量在導(dǎo)出時(shí)刻的拷貝)
ES Modules 實(shí)現(xiàn):
// lib.mjs
export let age = 18;
export function addAge() {
age++;
}
// main.mjs
import { age, addAge } from './lib.mjs';
console.log(age); // 18
addAge();
console.log(age); // 19 (注意:這里變成了 19,因?yàn)?import 獲取的是實(shí)時(shí)的綁定)
技術(shù)延伸:
由于 ESM 是實(shí)時(shí)引用,它能更好地處理循環(huán)依賴問題。在 ESM 中,只要引用存在,代碼就能執(zhí)行(盡管可能在暫時(shí)性死區(qū) TDZ 中);而在 CJS 中,循環(huán)依賴可能導(dǎo)致導(dǎo)出一個(gè)不完整的對(duì)象(空對(duì)象),因?yàn)槟K可能尚未執(zhí)行完畢。此外,ESM 導(dǎo)入的變量是只讀的(Read-only),嘗試在 main.mjs 中直接執(zhí)行 age = 20 會(huì)拋出 TypeError。
2. 加載時(shí)機(jī):運(yùn)行時(shí) vs 編譯時(shí)
CommonJS (運(yùn)行時(shí)) :
require 本質(zhì)上是一個(gè)函數(shù)。你可以將它放在 if 語句中,或者根據(jù)變量動(dòng)態(tài)生成路徑。只有當(dāng)代碼執(zhí)行到這一行時(shí),Node.js 才會(huì)去加載模塊。if (condition) { const lib = require('./lib.js'); // 條件加載 }ES Modules (編譯時(shí)) :
import 語句(靜態(tài)導(dǎo)入)必須位于模塊頂層,不能嵌套在代碼塊中。JavaScript 引擎在編譯階段(解析 AST 時(shí))就能確定模塊的依賴關(guān)系。
工程化價(jià)值:這使得 Tree Shaking(搖樹優(yōu)化) 成為可能。構(gòu)建工具可以在打包時(shí)靜態(tài)分析出哪些 export 沒有被使用,從而安全地刪除這些死代碼,減小包體積。
3. 加載方式:同步 vs 異步
- CommonJS (同步) :
主要用于服務(wù)器端。文件都在本地磁盤,讀取時(shí)間通常在毫秒級(jí),同步加載不會(huì)造成明顯的性能瓶頸。 - ES Modules (異步) :
設(shè)計(jì)之初就考慮了瀏覽器環(huán)境。在瀏覽器中,模塊需要通過網(wǎng)絡(luò)請(qǐng)求加載,網(wǎng)絡(luò)延遲不可控。如果采用同步加載,會(huì)阻塞主線程,導(dǎo)致頁(yè)面“假死”無法交互。因此,ESM 規(guī)范規(guī)定模塊解析階段是異步的。
四、工程化實(shí)踐與互操作性
在 Node.js 環(huán)境逐步過渡到 ESM 的過程中,兩者共存的情況十分常見。
1. 文件后綴與配置
在 Node.js 中,為了區(qū)分模塊類型:
- CommonJS:通常使用 .cjs 后綴,或者在 package.json 中未設(shè)置 type 字段(默認(rèn)為 CJS)。
- ES Modules:強(qiáng)制使用 .mjs 后綴,或者在 package.json 中設(shè)置 "type": "module"。
2. 相互引用(Interoperability)
這是開發(fā)中最容易踩坑的地方。
場(chǎng)景 A:CommonJS 引用 ES Modules
由于 CJS 是同步的 require,而 ESM 是異步加載的,因此原生 CJS 無法直接 require ESM 文件。
常規(guī)方案:使用異步的動(dòng)態(tài)導(dǎo)入 import() 配合 IIFE。
// index.cjs (async () => { const { default: foo } = await import('./foo.mjs'); })();新特性(Node.js v22+ / Experimental) :
Node.js 在 2024 年推出了 --experimental-require-module 標(biāo)志。開啟后,支持同步 require 加載 ESM(前提是該 ESM 模塊內(nèi)部沒有頂級(jí) await)。node --experimental-require-module index.cjs
場(chǎng)景 B:ES Modules 引用 CommonJS
ESM 的兼容性較好,可以導(dǎo)入 CJS 模塊。
機(jī)制:Node.js 會(huì)將 CJS 的 module.exports 整體作為一個(gè)默認(rèn)導(dǎo)出(Default Export)處理。
注意事項(xiàng):不支持具名導(dǎo)入(Named Imports)的直接解構(gòu)。雖然部分構(gòu)建工具(如 Webpack)支持混用,但在原生 Node.js 環(huán)境下,以下寫法通常會(huì)報(bào)錯(cuò)或表現(xiàn)不符合預(yù)期:
// 錯(cuò)誤示范 (原生 Node.js) import { someMethod } from './lib.cjs'; // 可能會(huì)失敗,因?yàn)?CJS 只有 default 導(dǎo)出正確寫法:
import lib from './lib.cjs'; const { someMethod } = lib;
五、面試場(chǎng)景復(fù)盤
面試官提問:“請(qǐng)聊聊 CommonJS 和 ESM 的區(qū)別。”
高分回答策略:
1. 一句話定性(宏觀視角)
“CommonJS 是 Node.js 社區(qū)提出的服務(wù)器端運(yùn)行時(shí)模塊規(guī)范,主要特點(diǎn)是同步加載和值的拷貝;而 ES Modules 是 ECMAScript 的官方標(biāo)準(zhǔn),實(shí)現(xiàn)了瀏覽器和服務(wù)端的統(tǒng)一,主要特點(diǎn)是編譯時(shí)靜態(tài)分析、異步加載和值的引用。”
2. 核心差異展開(技術(shù)深度)
“兩者最本質(zhì)的區(qū)別在于輸出值的機(jī)制。
CommonJS 輸出的是值的拷貝。一旦模塊輸出,內(nèi)部變量的變化不會(huì)影響導(dǎo)出值,類似于基本類型的賦值。
ES Modules 輸出的是值的引用(Live Binding) 。導(dǎo)入的變量實(shí)際上是指向模塊內(nèi)部?jī)?nèi)存地址的指針,模塊內(nèi)部變化會(huì)實(shí)時(shí)反映到外部,這使得 ESM 能更好地處理循環(huán)依賴問題。”
3. 工程化價(jià)值(架構(gòu)視角)
“在工程實(shí)踐中,ESM 的靜態(tài)編譯特性非常關(guān)鍵。因?yàn)樗试S構(gòu)建工具在代碼運(yùn)行前分析依賴關(guān)系,從而實(shí)現(xiàn) Tree Shaking,去除無用代碼,優(yōu)化包體積。這是 CommonJS 這種動(dòng)態(tài)加載規(guī)范無法做到的。”
4. 兼容性補(bǔ)充(實(shí)戰(zhàn)經(jīng)驗(yàn))
“在 Node.js 環(huán)境中,兩者互操作需要注意。ESM 可以較容易地導(dǎo)入 CJS(作為默認(rèn)導(dǎo)出),但 CJS 導(dǎo)入 ESM 通常需要異步的 import()。不過,Node.js 最近引入了 --experimental-require-module 標(biāo)志,正嘗試打破這一同步加載的壁壘。”
到此這篇關(guān)于JavaScript中CommonJS 與 ES Modules的區(qū)別的文章就介紹到這了,更多相關(guān)JavaScript CommonJS ES Modules內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Javascript中正則表達(dá)式的應(yīng)用詳解
這篇文章主要為大家詳細(xì)介紹了Javascript中正則表達(dá)式的應(yīng)用,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下,希望能夠給你帶來幫助2022-02-02
js點(diǎn)擊事件的執(zhí)行過程實(shí)例分析【冒泡與捕獲】
這篇文章主要介紹了js點(diǎn)擊事件的執(zhí)行過程,結(jié)合實(shí)例形式分析了js事件機(jī)制中的冒泡與捕獲相關(guān)原理、操作技巧與注意事項(xiàng),需要的朋友可以參考下2020-04-04
微信小程序?qū)崿F(xiàn)一個(gè)自定義遮罩層效果
這篇文章主要介紹了微信小程序?qū)崿F(xiàn)一個(gè)自定義遮罩層,大概效果是點(diǎn)擊按鈕Show顯示遮罩層,再次點(diǎn)擊屏幕任何地方隱藏遮罩層,本文結(jié)合實(shí)例代碼給大家介紹的非常詳細(xì),需要的朋友可以參考下2023-09-09
Javascript讀取上傳文件內(nèi)容/類型/字節(jié)數(shù)
這篇文章主要為大家詳細(xì)介紹了Javascript讀取上傳文件內(nèi)容/類型/字節(jié)數(shù),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-04-04
javascript下高性能字符串連接StringBuffer類
使用StringBuffer類比使用加號(hào)節(jié)省50%左右的時(shí)間,大家對(duì)于大數(shù)據(jù)的連接最好使用這個(gè)方法。2010-08-08
淺談高大上的微信小程序中渲染html內(nèi)容—技術(shù)分享
大部分Web應(yīng)用的富文本內(nèi)容都是以HTML字符串的形式存儲(chǔ)的,那么在微信小程序中,應(yīng)當(dāng)如何渲染這部分內(nèi)容呢?感興趣的小伙伴們可以參考一下2018-10-10
Javascript驗(yàn)證上傳圖片大小[前臺(tái)處理]
在做上傳圖片的時(shí)候,如果不限制上傳圖片大小,后果非常的嚴(yán)重。解決這個(gè)問題有兩種方式:后臺(tái)處理、前臺(tái)處理2014-07-07

