關(guān)于 Commonjs 和 ES module 模塊導(dǎo)出的區(qū)別,一般流行一種說(shuō)法:CommonJS 模塊輸出的是一個(gè)值的拷貝,ES6 模塊輸出的是值的引用,而我發(fā)現(xiàn),絕大部分用于證明 Commonjs 模塊導(dǎo)出值的例程都是有問(wèn)題的,我們一起來(lái)看下:
// 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 被賦值為一個(gè)對(duì)象(暫稱(chēng)為導(dǎo)出對(duì)象),而導(dǎo)出對(duì)象的 count 屬性源自 count 變量,由于 count 變量是數(shù)值類(lèi)型,屬于 js 的基本類(lèi)型之一,是按值傳遞的,所以 count 屬性得到的只是 count 變量的拷貝值,也就是說(shuō)從賦值之后開(kāi)始 count 變量的任何變化都與導(dǎo)出對(duì)象的 count 屬性毫無(wú)關(guān)系。so,這個(gè)例程根本證明不了 Commonjs 模塊導(dǎo)出值是值的拷貝還是引用。
為了確保嚴(yán)謹(jǐn)性,我們跑一遍該 demo 在 ES module 下的實(shí)現(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)出語(yǔ)法,這易于證明 ES module 模塊導(dǎo)出是值的引用,在原始值改變時(shí) import 的加載值也會(huì)隨之變化:
// 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 變量自增,這個(gè)變化能在 a 模塊中體現(xiàn),這是由于 b 模塊中 count 變量和導(dǎo)出的 count 共用同一個(gè)內(nèi)存空間(準(zhǔn)確地說(shuō),是模塊 export 連接的內(nèi)存空間地址就是 count 變量的內(nèi)存地址),所以說(shuō) ES module 導(dǎo)出是值的引用。至于詳細(xì)的導(dǎo)出原理,大家可以瀏覽這篇文章中對(duì)于 ES module 原理的闡述:Commonjs、esm、Amd和Cmd的循環(huán)依賴(lài)表現(xiàn)和原理。
那么問(wèn)題來(lái)了,我們應(yīng)該如何證明 Commonjs 模塊導(dǎo)出是值的拷貝呢?
目前想到了兩個(gè)比較靠譜的方案:
- 直接翻看
node中關(guān)于Module類(lèi)的源碼實(shí)現(xiàn); - 參考 Webpack 等構(gòu)建工具是如何處理
Commonjs模塊的;
第一種方案后續(xù)會(huì)找時(shí)間剖析源碼給大家分享,我們先來(lái)瞧瞧 Webpack 是如何構(gòu)建下面的 Commonjs 模塊 demo 的:
// a.js
const b = require('./b');
console.log(b.count);
// b.js
module.exports = {
count: 1,
};
Webpack 輸出的 bundle,這里省去了注釋和部分無(wú)關(guān)代碼:
(function(modules) {
// webpackBootstrap
// ...
// webpack實(shí)現(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)鍵點(diǎn):模塊導(dǎo)出預(yù)置了一個(gè)空對(duì)象
};
// 模塊代碼執(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)出在這里其實(shí)只是對(duì) installedModules[moduleId].exports 屬性的賦值操作,所以針對(duì)以下情況:
// 在預(yù)置的`installedModules[moduleId].exports`空對(duì)象上新增一個(gè)基本類(lèi)型的`count`屬性,相當(dāng)于基本類(lèi)型的拷貝。
let count = 1;
exports.count = count;
// `installedModules[moduleId].exports`被賦值一個(gè)新的包含`count`屬性的對(duì)象,相當(dāng)于對(duì)象淺拷貝。
module.exports = {
count,
};
這就可以說(shuō)明 Commonjs 模塊導(dǎo)出的是值的拷貝了。