關(guān)于 Commonjs
和 ES module
模塊導(dǎo)出的區(qū)別个唧,一般流行一種說法:CommonJS 模塊輸出的是一個值的拷貝,ES6 模塊輸出的是值的引用设预,而我發(fā)現(xiàn)徙歼,絕大部分用于證明 Commonjs
模塊導(dǎo)出值的例程都是有問題的,我們一起來看下:
// b.js
let count = 1;
module.exports = {
count,
add() {
count++;
},
get() {
return count;
}
};
// a.js
const { count, add, get } = require('./b');
console.log(count); // 1
add();
console.log(count); // 1
console.log(get()); // 2
b.js
中,module.exports
被賦值為一個對象(暫稱為導(dǎo)出對象)魄梯,而導(dǎo)出對象的 count
屬性源自 count
變量桨螺,由于 count
變量是數(shù)值類型,屬于 js 的基本類型之一酿秸,是按值傳遞的灭翔,所以 count
屬性得到的只是 count
變量的拷貝值,也就是說從賦值之后開始 count
變量的任何變化都與導(dǎo)出對象的 count
屬性毫無關(guān)系辣苏。so肝箱,這個例程根本證明不了 Commonjs
模塊導(dǎo)出值是值的拷貝還是引用。
為了確保嚴謹性稀蟋,我們跑一遍該 demo 在 ES module
下的實現(xiàn)煌张,看看輸出是否是一致的:
// b.mjs
let count = 1;
export default {
count,
add() {
count++;
},
get() {
return count;
}
}
// a.mjs
import b from './b.mjs';
console.log(b.count); // 1
b.add();
console.log(b.count); // 1
console.log(b.get()); // 2
與 Commonjs
提供的導(dǎo)出規(guī)范不同,ES module
支持以下的導(dǎo)出語法退客,這易于證明 ES module
模塊導(dǎo)出是值的引用骏融,在原始值改變時 import
的加載值也會隨之變化:
// b.mjs
export let count = 1;
export function add() {
count++;
}
export function get() {
return count;
}
// a.mjs
import { count, add, get } from './b.mjs';
console.log(count); // 1
add();
console.log(count); // 2
console.log(get()); // 2
上面代碼中,add
函數(shù)執(zhí)行使 count
變量自增萌狂,這個變化能在 a
模塊中體現(xiàn)绎谦,這是由于 b
模塊中 count
變量和導(dǎo)出的 count
共用同一個內(nèi)存空間(準確地說,是模塊 export
連接的內(nèi)存空間地址就是 count
變量的內(nèi)存地址)粥脚,所以說 ES module
導(dǎo)出是值的引用窃肠。至于詳細的導(dǎo)出原理,大家可以瀏覽這篇文章中對于 ES module
原理的闡述:Commonjs刷允、esm冤留、Amd和Cmd的循環(huán)依賴表現(xiàn)和原理。
那么問題來了树灶,我們應(yīng)該如何證明 Commonjs
模塊導(dǎo)出是值的拷貝呢纤怒?
目前想到了兩個比較靠譜的方案:
- 直接翻看
node
中關(guān)于Module
類的源碼實現(xiàn); - 參考 Webpack 等構(gòu)建工具是如何處理
Commonjs
模塊的天通;
第一種方案后續(xù)會找時間剖析源碼給大家分享泊窘,我們先來瞧瞧 Webpack 是如何構(gòu)建下面的 Commonjs
模塊 demo 的:
// a.js
const b = require('./b');
console.log(b.count);
// b.js
module.exports = {
count: 1,
};
Webpack 輸出的 bundle,這里省去了注釋和部分無關(guān)代碼:
(function(modules) {
// webpackBootstrap
// ...
// webpack實現(xiàn)的require函數(shù)
function __webpack_require__(moduleId) {
if(installedModules[moduleId]) {
return installedModules[moduleId].exports;
}
// 模塊緩存id像寒、加載狀態(tài)和導(dǎo)出值
var module = installedModules[moduleId] = {
i: moduleId,
l: false,
exports: {} // 關(guān)鍵點:模塊導(dǎo)出預(yù)置了一個空對象
};
// 模塊代碼執(zhí)行
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
module.l = true;
return module.exports;
}
// ...
return __webpack_require__(__webpack_require__.s = 0);
})([
// a.js
(function(module, exports, __webpack_require__) {
const b = __webpack_require__(1);
console.log(b.count);
}),
// b.js
(function(module, exports) {
module.exports = {
count: 1,
};
})
])
從編譯后的 bundle 看出烘豹,Commonjs
模塊導(dǎo)出在這里其實只是對 installedModules[moduleId].exports
屬性的賦值操作,所以針對以下情況:
// 在預(yù)置的`installedModules[moduleId].exports`空對象上新增一個基本類型的`count`屬性诺祸,相當于基本類型的拷貝携悯。
let count = 1;
exports.count = count;
// `installedModules[moduleId].exports`被賦值一個新的包含`count`屬性的對象,相當于對象淺拷貝筷笨。
module.exports = {
count,
};
這就可以說明 Commonjs
模塊導(dǎo)出的是值的拷貝了憔鬼。