在我的Node.js - 模組系統 (一)文章裡, 初步的認識了 Node.js 模組系統的運作原理, 在這篇文章中, 將更深入探討模組系統到底是怎麼一回事。
CommonJS 模組規範
瀏覽器中執行的 JavaScript 是不支援模組化的, 但在 Node.js 中參照 CommonJS 規範
, 設計了一套模組化系統
- 模組(文件)作用域 (每個文件的成員都是封閉的, 預設無法互相取用, 需透過導出和載入)
- 通信規則
- 使用
require
來載入模組 - 使用
exports
接口物件來導出模組中成員
- 使用
關於瀏覽器中的 JS 及 Node.js 的比較, 請參考認識 Node.js
exports 物件和 module.exports 物件差異
在Node.js - 模組系統 (一)中, 導出模組成員時是使用 exports
物件做導出的, 如以下範例
B.js 導出成員
1 | // B.js |
A.js 引入成員
1 | // A.js |
終端機執行 node A, 成功的取得 B 導出的成員
1 | { aaa: 'aaa', bbb: [Function] } |
在上一篇沒有提到的是, exports 其實是一個簡化的寫法, 在 Node 模組系統中, 真正被導出的物件其實叫做 module.exports
, 下面來做個實驗看看
B.js 導出成員
1 | // B.js |
A.js 引入成員
1 | // A.js |
終端機執行 node A, 得到的結果和使用 exports 物件導出是一樣的
1 | { aaa: 'aaa', bbb: [Function] } |
嗯? 我說那個原理呢?
若你對 JavaScript 有基本的認識, 應該對 by reference (傳參考)的概念不陌生, 其實就是透過這個原理做到的。
在 JS 中物件型別特性是傳參考,若將一個A變數指向某個物件,當修改A變數或是指向的那個物件內部任一屬性,都會影響另一個物件,因為現在這兩個變數都被指向同一個記憶體位置。
範例
1 | var obj1 = { name: 'dylan', age: 18 }; |
了解這個概念後, 你可以想像一件事, Node.js 在模組系統的底層寫了這麼一段程式碼
1 | var module = { |
module.exports
是被預設的接口物件, 每一個 Node.js 的模組底層都有這麼一個空物件,你可以想像, 在程式碼的最後 Node 會將 module.exports 物件 return
1 | return module.exports |
之後誰來 require 這個模組, 誰就可以得到這個 module.exports 物件。
but 這個物件的名稱實在是太長了, 如果要導出多個物件你可能需要
1 | module.exports.aaa = 'aaa' |
當然你也可以這麼做啦
1 | module.exports = { |
不過貼心如 Node, 還是為你做了一件事情
1 | let exports = module.exports |
它宣告了一個新變數 exports
指向了 module.exports 接口物件的記憶體, 所以透過傳參考的特性, 不管你對兩個中的哪個物件新增成員, 他們都會同步被改變。關於這點你可以在任一 JS 檔貼上下面這段程式碼, 並且在 Node 環境執行它, 你會得到 true
的結果
1 | console.log(exports === module.exports) |
好的, 了解這個概念後, 現在你可以簡化成
1 | exports.aaa = 'aaa' |
但是你千萬不能這麼做
1 | exports = { |
exports 物件陷阱
為什麼不能這麼做呢? 來實際實驗看看
B.js
1 | exports = { |
A.js
1 | let B_exports = require('./B') |
終端機執行 node A, 得到的結果
1 | {} |
奇怪了? exports 不是等同於 module.exports 嗎? 為什麼引入模組得到的是空物件?
其實, 如果你真的理解 JavaScript 傳參考的特性, 就有答案了。
來看看一個簡單傳參考範例
首先先建立一個傳參考物件
1 | let obj1 = { name: 'dylan', age: 18 } |
輸出結果
1 | {name: 'dylan', age: 18, hobby: 'coding'} // obj1 |
到目前為止沒有問題, 但是若你對 obj2 物件重新賦值( 也是就重新讓 obj2 等於
某某東西時 )
1 | obj2 = { |
輸出結果, obj1 並沒有被影響
1 | {name: "dylan", age: 18, hobby: "coding"} |
當你對傳參考物件(obj2)重新賦值時, 它的記憶體位置已經不再指向和 obj1 所指向的 {name: 'dylan', age: 18, hobby: 'coding'
物件了, 它已是另外一個獨立的物件, 也就是說傳參考的連結在你重新賦值
時即斷開。
所以, 在 Node.js 中, 請不要對 exports 物件做任何
重新賦值
的行為, 如果有需要只能使用module.exports
物件。
總結實戰時最常用使用方式:
- 導出多個成員 (也就是導出一個物件, 成員都放在裡頭)
exports.XXX = xxx
或module.exports.XXX = xxx
都可以module.exports = { ... }
(只能是 module.exports)
- 導出單個成員 (無論指向什麼型別的東西, 導出的東西就是它一個)
module.exports = XXX
(只能是 module.exports)
最後, 若對上列的描述還是覺得不夠清楚, 你可以再看一次(毆).. 不是啦, 如果你覺得要理解這些有點麻煩, 就一律都使用 module.exports
就可以避免所有問題了。