如何正確證明 Commonjs 模塊導(dǎo)出是值的拷貝,而 ES module 是值的引用?

關(guān)于 CommonjsES 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)出的是值的拷貝了。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容