細讀 JS | JavaScript 模塊化之路

配圖源自 Freepik

學習不能停,都給我卷起來...

一、前世今生

在 ES6 之前,JavaScript 一直沒有官方的模塊(Module)體系,對于開發大型、復雜的項目形成了巨大的障礙。幸好社區上有一些模塊加載方案,最主要的有 CommonJS(CommonJS Modules)和 AMD(Asynchronous Module Definition)兩種模塊規范,前者用于服務器,后者用于瀏覽器。

隨著 ES6 的正式發布,全新的模塊將逐步取代 CommonJS 和 AMD 規范,成為瀏覽器和服務器通用的模塊解決方案。

ES6 模塊的設計思想盡量的靜態化,是的編譯時就能確定模塊的依賴關系,以及輸入和輸出的變量。

rollupwebpack 等構建工具中常見的 Tree Shaking 能力,就是依賴于 ES6 模塊的靜態特性實現的。

而 CommonJS 和 AMD 模塊都只能在運行時確定這些東西。比如,CommonJS 模塊就是對象,輸入時必須查找對象屬性。

// CommonJS 模塊
const { stat, exists, readFile } = require('fs')

// 相當于
const _fs = require('fs')
const stat = _fs.stat
const exists = _fs.exists
const readFile = _fs.readFile

以上示例,實際上是整體加載了 fs 模塊(即加載 fs 的所有方法),生成了一個對象 _fs,然后再從這個對象上讀取了 3 個方法。這種方式稱為“運行時加載”,原因是只有運行時才能得到這個對象,導致完全沒有辦法在編譯時做“靜態優化”。

ES6 模塊不是對象,而是通過 export 命令顯式指定輸出的代碼,再通過 import 命令輸入。

import { stat, exists, readFile } from 'fs'

以上示例,實際上是從 fs 模塊中加載了 3 個方法,其他方法不加載。這種方式稱為“編譯時加載”或“靜態加載”,即 ES6 模塊可以在編譯時就完成模塊加載,效率要高于 CommonJS 模塊的加載方式。這也導致了沒法引用 ES6 模塊本身,因為它不是對象。

由于 ES6 模塊是編譯時加載,使得靜態分析成為可能。有了它,就能進一步拓寬 JavaScript 的語法,比如引入宏(macro)和類型檢驗(type system)這些只能靠靜態分析實現的功能。

除了靜態加載帶來的各種好處,ES6 模塊還有以下好處:

  • 不再需要 UMD 模塊格式了,將來服務器和瀏覽器都會支持 ES6 模塊格式。
  • 將來瀏覽器的新 API 就能用模塊格式提供,不再必須做成全局變量或者 navigator 對象的屬性。
  • 不再需要對象作為命名空間(比如 Math 對象),未來這些功能可以通過模塊提供。

二、為什么需要模塊化?

舉個 ??

<!DOCTYPE html>
<html lang="en">
  <body>
    <script src="module-a.js"></script>
    <script src="module-b.js"></script>
  </body>
</html>
// module-a.js
var person = { name: 'Frankie', age: 20 }
// module-b.js
console.log(person.name) // 將會打印什么呢?

我們可以輕而易舉就知道 module-b.js 里將會打印出 Frankie,原因很簡單,它們都是處于全局作用域下,因此 module-b.js 中的 person.name 就能讀取到在 module-a.js 中定義的 person 變量。

如果將 module-a.jsmodule-b.js 在 HTML 中的順序換過來,就會拋出錯誤。原因是<script> 是按塊加載的,包括下載、(預)編譯和執行。唯有當前塊執行完畢,或者拋出錯誤,才會接著加載下一個 <script>

注意,這里提到的按順序加載,是指沒有 deferasync 屬性的哈。它倆對外部腳本的加載方式是有影響的。但非本文話題,因此不展開講述。

那問題就來了,這很容易造成全局污染,對于大型、復雜的項目來說會非常棘手。

假設沒有諸如 CommonJS 等模塊化解決方案可用,要怎樣解決這種問題呢?

1. 對象字面量(Object Literal)

// 聲明
var namespace = {
  prop: 123,
  method: function () {},
  // ...
}

// 調用
namespace.prop
namespace.method()

缺點:

作為一個單一的、有時很長的句法結構,它對其內容施加了限制。內容必須在 {} 之間,并且屬性或方法之間必須添加逗號。當模塊內容復雜起來之后,維護成本高,移動內容變得更加困難。

在多個文件中使用相同的命名空間:可以將模塊定義分散到多個文件中,并按如下方式創建命名空間變量,則可忽視加載文件的順序。

var namespace = namespace || {}

使用多個模塊,可以通過創建單個全局命名空間并向其添加子模塊來避免全局名稱的擴散。不建議進一步嵌套,如果名稱沖突是一個問題,您可以使用更長的名稱。這種方式稱為:嵌套命名空間。

// 全局命名空間
var globalns = globalns || {}

// 添加 A 子模塊
globalns.moduleA = {
  // module content
}

// 添加 B 子模塊
globalns.moduleB = {
  // module content
}

盡管使用命名空間可以在一定程度上解決了命名沖突的問題,但是存在一個問題:在 moduleB 中可以修改 moduleA 的內容,而且 moduleA 可能還蒙在鼓里,不知情。

以上命名空間內的所有成員和方法,無論是否私有,對外都是可訪問的。這是一個明顯的缺點,模塊化不應該如此設計。

Yahoo 公司的 YUI 2 就是采用了這種方案。

2. 立即執行函數表達式(Immediately-Invoked Function Expression,簡稱 IIFE)

在模塊模式中,我們使用 IIFE 將環境附加到模塊數據。可以從模塊訪問該環境內的綁定,但不能從外部訪問。另一個優點是 IIFE 為我們提供了執行初始化的地方。

var namespace = (function () {
  // private data
  var _prop = 123
  var _method = function () {}

  return {
    // read-only
    get prop() {
      return _prop
    },
    get method() {
      return _method
    }
  }
})()

這樣的話,我們就不用擔心,在外部直接修改 namespace 內部的成員或者方法了。

// 讀取
namespace.prop // 123
namespace.method() 

// 寫入
namespace.prop = 456 // 無效
namespace.method = function foo() {} // 無效

因此,結合前面的內容,就可以這樣去處理:

// 全局命名空間
var globalns = globalns || {}

// 添加 A 子模塊
globalns.moduleA = (function () {
  // ...

  return {
    // ...
  }
})()

// 添加 B 子模塊
globalns.moduleB = (function () {
  // ...

  return {
    // ...
  }
})()

到現在,有了命名空間解決了命名沖突問題,同時使用 IIFE 來維護各模塊的私有成員和方法,導出對外的開放接口即可。這似乎有了模塊化該有的樣子。

但是,還有一個問題。前面提到過 <script> 是按書寫順序加載的(即使下載順序可能并行的),主要包括:

  • 腳本下載
  • 腳本解析(編譯和執行)

假設我們的腳本如下:

<!DOCTYPE html>
<html lang="en">
  <body>
    <script src="module-a.js"></script>
    <script src="module-b.js"></script>
  </body>
</html>

那么我們的 modueA 在(首次)解析的時候,就沒辦法調用 moduleB 的內容,因為它壓根還沒解析執行。一旦項目復雜度、模塊數量上來之后,模塊之間的依賴關系就很難維護了。

三、社區模塊化方案

在 ES2015 之前,社區上已經有了很多模塊化方案,流行的主要有以下幾個::

  • CommonJS
  • AMD(Asynchronous Module Definition)
  • CMD(Common Module Definition)
  • UMD(Universal Module Definition)

其中 CommonJS 規范在 Node.js 環境下取得了很不錯的實踐,它只能應用于服務器端,而不支持瀏覽器環境。CommonJS 規范的模塊是同步加載的,由于服務器的模塊文件存在于本地硬盤,只有磁盤 I/O 的,因此同步加載機制沒什么問題。

但在瀏覽器環境,一是會產生開銷更大的網絡 I/O,二是天然異步,就會產生時序上的錯誤。后來社區上推出了異步加載、可在瀏覽器環境運行的 RequireJS 模塊加載器,不久之后,起草并發布了 AMD 模塊化標準規范。

由于 AMD 會提前加載,很多開發者擔心有性能問題。假設一個模塊依賴了另外 5 個模塊,不管這些模塊是否馬上被用到,都會執行一遍,這些性能消耗是不容忽視的。為了避免這個問題,有部分人試圖保留 CommonJS 書寫方式和延遲加載、就近聲明(就近依賴)等特性,并引入異步加載機制,以適配瀏覽器特性。比如,已經涼涼的 BravoJS、FlyScript 等方案。

在 2011 年,國內的前端大佬玉伯提出了 SeaJS,它借鑒了 CommonJS、AMD,并提出了 CMD 模塊化標準規范。但并沒有大范圍的推廣和使用。

在 2014 年,美籍華裔 Homa Wong 提出了 UMD 方案:將 CommonJS 和 AMD 相結合。本質上這不算是一種模塊化方案。

到了 2015 年 6 月,隨著 ECMAScript 2015 的正式發布,JavaScript 終于原生支持模塊化,被稱為 ES Module。同時支持服務器端和瀏覽器端。

盡管到了 2022 年,現狀仍然是多種模塊化方案共存,但未來肯定是 ES Module 一統江湖...

關于 JavaScript 模塊化歷史線,可以看下這篇文章

四、CommonJS

Node.js 的模塊系統是基于 CommonJS 規范的實現的。除此之外,像 CouchDB 等也是 CommonJS 的一種實現。而且它們有一些是沒有完全按照 CommonJS 規范去實現的,甚至額外添加了特有的功能。

由于我們接觸到的 CommonJS 通常指 Node.js 中的模塊化解決方法,因此,接下來提到的 CommonJS 均指 Node.js 的模塊系統。

先瞅一下,一個 CommonJS 模塊里面都包括一些什么信息:

如果有一些看不懂或不了解其用處的,先不急,下面娓娓道來。

CommonJS 的模塊特點:

  • 每一個 JavaScript 文件就是一個獨立模塊,其作用域僅在模塊內,不會污染全局作用域。
  • 一個模塊包括 requiremoduleexports 三個核心變量。
  • 其中 module.exportsexports 負責模塊的內容導出。后者只是前者的“別名”,若使用不當,還可能會導致無法導出預期內容。其中 require 負責其他模塊內容的導入,而且其導入的是其他模塊的 module.exports 對象。
  • 模塊可以加載多次,但只會在第一次加載時運行一次,然后運行結果就會被緩存起來。下次再加載是直接讀取緩存結果。模塊緩存是可以被清除的。
  • 模塊的加載是同步的,而且是按編寫順序進行加載。

4.1 Module 對象

前面打印的 module 就是 Module 的實例對象。每個模塊內部,都有一個 module 對象,表示當前模塊。它有以下屬性:

// Module 構造函數
function Module(id = '', parent) {
  this.id = id
  this.path = path.dirname(id)
  this.exports = {}
  moduleParentCache.set(this, parent)
  updateChildren(parent, this, false)
  this.filename = null
  this.loaded = false
  this.children = []
}

源碼 ?? node/lib/internal/modules/cjs/loader.js(Node.js v17.x)

module.id:返回字符串,表示模塊的標識符,通常這是完全解析的文件名。
module.path:返回字符串,表示模塊的目錄名稱,通常與 module.idpath.dirname() 相同。
module.exports:模塊對外輸出的接口,默認值為 {}。默認情況下,module.exportsexports 是相等的。
module.filename:返回字符串,表示模塊的完全解析文件名(含絕對路徑)。
module.loaded:返回布爾值,表示模塊是否已完成加載或正在加載。
module.children:返回數組,表示當前模塊引用的其他模塊的實例對象。
module.parent:返回 null 或數組。若返回值為數組時,表示當前模塊被其他模塊引用了,而且每個數組元素表示被引用模塊對應的實例對象。
module.paths:返回數組,表示模塊的搜索路徑(含絕對路徑)。
module.isPreloading:返回布爾值,如果模塊在 Node.js 預加載階段運行,則為 true。

注意點

  • 賦值給 module.exports 必須立即完成,不能在任何回調中完成(應在同步任務中完成)。
    比如,在 setTimeout 回調中對 module.exports 進行賦值是“不起作用”的,原因是 CommonJS 模塊化是同步加載的。

請看示例:

// module-a.js
setTimeout(() => {
  module.exports = { welcome: 'Hello World' }
}, 0)

// module-b.js
const a = require('./a')
console.log(a.welcome) // undefined

// ? 錯誤示例

再看個示例:

// module-a.js
const EventEmitter = require('events')
module.exports = new EventEmitter() // 同步任務中完成對 module.exports 的賦值

setTimeout(() => {
  module.exports.emit('ready') // ? 這個會生效嗎?
}, 1000)

// module-b.js
const a = require('./module-a')
a.on('ready', () => {
  console.log('module a is ready')
})

// ?? 執行 `node module-b.js` 命令運行腳本,以上 ready 事件可以正常響應,
// 原因 require() 會對模塊輸出值進行“淺拷貝”,因此 module-a.js 中的 setTimeout 是可以更新 EventEmitter 實例對象的。
  • module.exports 屬性被新對象完全替換時,通常也會“自動”重新分配 exports(自動是指不顯式分配新對象給 exports 變量的前提下)。但是,如果使用 exports 變量導出新對象,則必須“手動”關聯 module.exprotsexports,否則無法按預期輸出模塊值。

請看示例:

// 1?? 以這種方式進行模塊的輸出,module.exports 與 exports 會自動分配,即 module.exports === exports
module.exports = {
  // ...
}

// 2?? 以這種方式導出的值,將會是空對象 {},而不是 { sayHi: <Function> }
// 此時 module.exports !== exports
exports = { sayHi: function () {} } // ?

// 3?? 解決以上問題,需要手動關聯 module.exprots 和 exports,使得二者相等
module.exports = exports = { sayHi: function () {} } // ?
  • 由以上示例也可以看出,require() 方法引用的是 module.exports 對象,而不是 exports 變量。
  • 利用 module.parent 返回 null 或數值的特性,可以判斷當前模塊是否為入口腳本。另外,也可以通過 require.main 來獲取入口腳本的實例對象。

module.exports 與 exports 的注意點

此前已寫過一篇文章去介紹它倆的區別了。

一句話總結:exports 變量只是 module.exports 屬性的一個別名,僅此而已。

我們可以這樣對模塊進行輸出:

module.exports = {
  name: 'Frankie',
  age: 20,
  sayHi: () => console.log('Hi~')
}

// 相當于
exports.name = 'Frankie'
exports.age = 20
exports.sayHi = () => console.log('Hi~')

但請注意,若模塊只對外輸出一個接口,使用不當,可能會無法按預期工作。比如:

// ? 以下模塊的輸出是“無效”的,最終輸出值仍是 {}
exports = function () { console.log('Hi~') }

原因很簡單,在默認情況下 module.exports 屬性和 exports 變量都是同一個空對象 {}(默認值)的引用(reference),即 module.exports === exports

當對 exports 變量重新賦予一個基本值或引用值的時候, module.exportsexports 之間的聯系被切斷了,此時 module.exports !== exports,在當前模塊下 module.exports 的值仍為 {},而 exports 變量的值變為函數。而 require() 方法的返回值是所引用模塊的 module.exports 的淺拷貝結果。

正確姿勢應該是:

module.exports = export = function () { console.log('Hi~') } // ?

使用類似處理,使得 module.exportsexports 重新建立關聯關系。

這里并不存在任何難點,僅僅是 JavaScript 基本數據類型和引用數據類型的特性罷了。如果你還是分不清楚的話,建議只使用 module.exports 進行導出,這樣的話,就不會有問題了。

4.2 require 查找算法

require() 參數很簡單,那么 require() 內部是如何查找模塊的呢?

簡單可以分為幾類:

  • 加載 Node 內置模塊
    形式如:require('fs')require('http') 等。

  • 相對路徑、絕對路徑加載模塊
    形式如:require('./file')require('../file')require('/file')

  • 加載第三方模塊(即非內置模塊)
    形式如:require('react')require('lodash/debounce')require('some-library')require('#some-library') 等。

其中,絕對路徑形式在實際項目中幾乎不會使用(反正我是沒用過)、而 require('#some-library') 形式目前仍在試驗階段...

以下基于 Node.js 官網 相關內容翻譯并整理的版本(存檔

場景:在 `Y.js` 文件下,`require(X)`,Node.js 內部模塊查找算法:

1. 如果 `X` 為內置模塊的話,立即返回該模塊;

   因此,往 NPM 平臺上發包的話,`package.json` 中的 `name` 字段不能與 Node.js 內置模塊同名。

2. 如果 `X` 是以絕對路徑或相對路徑形式,根據 `Y` 所在目錄以及 `X` 的值以確定所要查找的模塊路徑(稱為 `Z`)。

  a. 將 `Z` 當作「文件」,按 `Z`、`Z.js`、`Z.json`、`Z.node` 順序查找文件,若找到立即返回文件,否則繼續往下查找;
  b. 將 `Z` 當作「目錄」,
     1)查找 `Z/package.json` 是否存在,若 `package.json` 存在且其 `main` 字段值不為虛值,將會按照其值確定模塊位置,否則繼續往下;
     2)按 `Z/index.js`、`Z/index.json`、`Z/index.node` 順序查找文件,若找到立即返回文件,否則會拋出異常 "not found"。

3. 若 `X` 是以 `#` 號開頭的,將會查找最靠近 `Y` 的 `package.json` 中的 `imports` 字段中 `node`、`require` 字段的值確認模塊的具體位置。
  (這一類現階段用得比較少,后面再展開介紹一下)
   // https://github.com/nodejs/node/pull/34117

4. 加載自身引用 `LOAD_PACKAGE_SELF(X, dirname(Y))`

    a. 如果當前所在目錄存在 `package.json` 文件,而且 `package.json` 中存在 `exports` 字段,
       其中 `name` 字段的值還要是 `X` 開頭一部分,
       滿足前置條件下,就會匹配 subpath 對應的模塊(無匹配項會拋出異常)。
      (這里提到的 subpath 與 5.b.1).1.1 類似)
    b. 若不滿足 a 中任意一個條件均不滿足,步驟 4 執行完畢,繼續往下查找。

5. 加載 node_modules `LOAD_NODE_MODULES(X, dirname(Y))`
   a. 從當前模塊所在目錄(即 `dirname(Y)`)開始,逐層查找是否 `node_modules/X` 是否存在,
      若找到就返回,否則繼續往父級目錄查找 `node_modules/X` ,依次類推,直到文件系統根目錄。
   b. 從全局目錄(指 `NODE_PATH` 環境變量相關的目錄)繼續查找。
  
   若 `LOAD_NODE_MODULES` 過程查找到模塊 X(可得到 X 對應的絕對路徑,假定為 M),將按以下步驟查找查找:
      1) 若 Node.js 版本支持 `exports` 字段(Node.js 12+),
          1.1 嘗試將 `M` 拆分為 name 和 subpath 形式(下稱 name 為 `NAME`)

              比如 `my-pkg` 拆分后,name 為 `my-pkg`,subpath 則為空(為空的話,對應  `exports` 的 "." 導出)。
              比如 `my-pkg/sub-module` 拆分后,name 為 `my-pkg`,subpath 為 `sub-module`。
              請注意帶 Scope 的包,比如 `@myorg/my-pkg/sub-module` 拆分后 name 應為 `@myorg/my-pkg`,subpath 為 `sub-module`。

          1.2 如果在 M 目錄下存在 `NAME/package.json` 文件,而且 `package.json` 的 `exports` 字段是真值,
              然后根據 subpath 匹配 `exports` 字段配置,找到對應的模塊(若 subpath 匹配不上的將會拋出異常)。
              請注意,由于 `exports` 支持條件導出,而且這里查找的是 CommonJS 模塊,
              因此 `exports` 的 `node`、`require`、`default` 字段都是支持的,鍵順序更早定義的優先級更高。

          1.3 如果以上任意一個條件不滿足的話,將繼續執行 2) 步驟

      2) 將 X 以絕對路徑的形式查找模塊(即前面的步驟 2),若找不到步驟 5 執行完畢,將會跑到步驟 6。

6. 拋出異常 "not found"

如果不是開發 NPM 包,在實際使用中的話,要不并沒有以上那么多復雜的步驟,很容易理解。但深入了解之后有助于平常遇到問題更快排查出原因并處理掉。如果你是發包的話,可以利用 exports 等按一定的策略導出模塊。

想了解 Node.js package.json 的兩個字段的意義,請看:

4.3 require 源碼

源碼 ?? node/lib/internal/modules/cjs/loader.js(Node.js v17.x)

// Loads a module at the given file path. Returns that module's `exports` property.
Module.prototype.require = function (id) {
  validateString(id, 'id')
  if (id === '') {
    throw new ERR_INVALID_ARG_VALUE('id', id, 'must be a non-empty string')
  }
  requireDepth++
  try {
    return Module._load(id, this, /* isMain */ false)
  } finally {
    requireDepth--
  }
}

源碼 ?? node/lib/internal/modules/cjs/loader.js(Node.js v17.x)

/**
 * 檢查所請求文件的緩存
 * 1. 如果緩存中已存在請求的文件,返回其導出對象(module.exports)
 * 2. 如果請求的是原生模塊,調用 `NativeModule.prototype.compileForPublicLoader()` 并返回其導出對象
 * 3. 否則,為該文件創建一個新模塊并將其保存到緩存中。 然后讓它在返回其導出對象之前加載文件內容。
 */
Module._load = function (request, parent, isMain) {
  let relResolveCacheIdentifier
  if (parent) {
    debug('Module._load REQUEST %s parent: %s', request, parent.id)
    // Fast path for (lazy loaded) modules in the same directory. The indirect
    // caching is required to allow cache invalidation without changing the old
    // cache key names.
    relResolveCacheIdentifier = `${parent.path}\x00${request}`
    const filename = relativeResolveCache[relResolveCacheIdentifier]
    if (filename !== undefined) {
      const cachedModule = Module._cache[filename]
      if (cachedModule !== undefined) {
        updateChildren(parent, cachedModule, true)
        if (!cachedModule.loaded) return getExportsForCircularRequire(cachedModule)
        return cachedModule.exports
      }
      delete relativeResolveCache[relResolveCacheIdentifier]
    }
  }

  // 1?? 獲取 require(id) 中 id 的絕對路徑(filename 作為模塊的標識符)
  const filename = Module._resolveFilename(request, parent, isMain)

  if (StringPrototypeStartsWith(filename, 'node:')) {
    // Slice 'node:' prefix
    const id = StringPrototypeSlice(filename, 5)

    const module = loadNativeModule(id, request)
    if (!module?.canBeRequiredByUsers) {
      throw new ERR_UNKNOWN_BUILTIN_MODULE(filename)
    }

    return module.exports
  }

  // 2?? 緩動是否存在緩存
  // 所有加載過的模塊都緩存于 Module._cache 中,以模塊的絕對路徑作為鍵值(cache key)
  const cachedModule = Module._cache[filename]

  if (cachedModule !== undefined) {
    updateChildren(parent, cachedModule, true)
    if (!cachedModule.loaded) {
      const parseCachedModule = cjsParseCache.get(cachedModule)
      if (!parseCachedModule || parseCachedModule.loaded) return getExportsForCircularRequire(cachedModule)
      parseCachedModule.loaded = true
    } else {
      // 若該模塊緩存過,則直接返回該模塊的 module.exports 屬性
      return cachedModule.exports
    }
  }

  // 3?? 加載 Node.js 原生模塊(內置模塊)
  const mod = loadNativeModule(filename, request)
  if (mod?.canBeRequiredByUsers) return mod.exports

  // 4?? 若請求模塊無緩存,調用 Module 構造函數生成模塊實例 module
  const module = cachedModule || new Module(filename, parent)

  // 如果是入口腳本,將入口模塊的 id 置為 "."
  if (isMain) {
    process.mainModule = module
    module.id = '.'
  }

  // 5?? 將模塊存入緩存中
  // ?????? 在模塊執行之前,提前放入緩存,以處理「循環引用」的問題
  // See, http://nodejs.cn/api/modules.html#cycles
  Module._cache[filename] = module
  if (parent !== undefined) {
    relativeResolveCache[relResolveCacheIdentifier] = filename
  }

  let threw = true
  try {
    // 6?? 執行模塊
    module.load(filename)
    threw = false
  } finally {
    if (threw) {
      delete Module._cache[filename]
      if (parent !== undefined) {
        delete relativeResolveCache[relResolveCacheIdentifier]
        const children = parent?.children
        if (ArrayIsArray(children)) {
          const index = ArrayPrototypeIndexOf(children, module)
          if (index !== -1) {
            ArrayPrototypeSplice(children, index, 1)
          }
        }
      }
    } else if (
      module.exports &&
      !isProxy(module.exports) &&
      ObjectGetPrototypeOf(module.exports) === CircularRequirePrototypeWarningProxy
    ) {
      ObjectSetPrototypeOf(module.exports, ObjectPrototype)
    }
  }

  // 7?? 返回模塊的輸出接口
  return module.exports
}

4.4 require 中幾個常見的問題

Q: Node.js 是如何實現同步加載機制的?
A:

未完待續...

References

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 1. CommonJS 規范(同步加載 NodeJS) 2. AMD(異步加載模塊 requireJS) 采用異步...
    菜雞前端閱讀 706評論 0 0
  • 前言 初期的web端交互還是很簡單,不需要太多的js就能實現。隨著時代的的發展,用戶對Web瀏覽器的性能也提出了越...
    菠菜女皇閱讀 472評論 0 0
  • ECMAScript 6(以下簡稱ES6)是JavaScript語言的下一代標準。因為當前版本的ES6是在2015...
    陳大沖閱讀 795評論 0 0
  • ECMAScript6(以下簡稱ES6)是JavScript語言的下一代標準。因為當前版本的ES6是在2015年發...
    imtns閱讀 489評論 0 0
  • ES6 代碼盡量標簽化語義化、優先使用標簽,class基于功能命名、基于內容命名、基于表現命布局提高css重用率,...
    唯軒_443e閱讀 342評論 0 0