前端模組化之CommonJS

RisingSunBlogs發表於2021-02-06

一、CommonJS特點

​ 經過前面討論,已經知道無模組化時專案中存在的問題。CommonJS的特點就是解決這些問題即:

​ 1.每個檔案都是一個單獨的模組,有自己的作用域,宣告的變數不是全域性變數(除非在模組內宣告的變數掛載到global上)

​ 2.每個檔案中的成員都是私有的,對外不可見

​ 3.A模組依賴B模組時,在A模組內部使用require函式引入B模組即可,模組之間依賴關係更加清晰

​ 4.模組的載入有快取機制,當載入完一次後,後續再載入就會讀取快取中的內容

​ 5.模組的載入順序是按照程式碼的書寫順序來載入

二、CommonJS的應用環境

​ 應用在Node.js中。CommonJS的載入機制是同步的,在Node環境中模組檔案是存在本地硬碟中,所以載入起來比較快,不用考慮非同步模式。

三、CommonJS的用法

​ 已經知到CommonJS規範下,每個檔案就是一個模組,都有私有作用域,那如何才能讓外部訪問某個模組中的內容呢?

​ 方式1:把模組中的成員掛載到global全域性物件中 【非常不推薦】

​ 方式2:使用模組的成員module.exports或者exports匯出成員

​ 最後在外部使用require函式引入所需模組

方式1:把變數掛載到global模組下【看完忘掉即可】

//module-b.js
var a = 10;
global.a = a;
//module-a.js
require("./module-b.js");
console.log(a); //10

方式2:使用module.exports匯出成員

1、使用module.exports單個匯出

//module-b.js
const a = 10;
const add = function(a, b) {
    return a + b;
}
module.exports.a = a;
module.exports.add = add;
//module-a.js
const moduleB = require("./module-b.js");

console.log(moduleB.a); //10
console.log(moduleB.add(1, 1)); //2

2、使用module.exports直接匯出一個物件

//module-b.js
const a = 10;
const add = function(a, b) {
    return a + b;
}
module.exports={a,add};
//module-a.js
const moduleB = require("./module-b.js");

console.log(moduleB.a); //10
console.log(moduleB.add(1, 1)); //2

3、使用exports代替module.exports來匯出成員

//module-b.js
const a = 10;
const add = function(a, b) {
    return a + b;
}
exports.a=a;
exports.add=add;
//使用exports時 不可使用下面這種方式匯出成員
exports={a,add}
//module-a.js
const moduleB = require("./module-b.js");

console.log(moduleB.a); //10
console.log(moduleB.add(1, 1)); //2

由程式碼可見,在使用module.exports和exports匯出成員時略有不同,具體是為什麼呢?稍後作出解釋

四、module.exports、exports和require為什麼可以直接使用?

從我們平時寫程式碼的經驗來看,在一個檔案中可以使用的成員由以下幾種情況:

1、全域性成員

2、在檔案內部宣告瞭該成員

但我們所瞭解的程式碼執行環境中的全域性成員只有一個,像window和global這種,那大概率不是全域性成員。而且我們在模組內部並未宣告這三個變數,那為何能直接使用呢?

其實在node執行環境中,每個模組都是執行在一個函式中,正是因為這個函式的存在,才讓每個模組有了私有作用域

(function (exports, require, module, __filename, __dirname) {
  // HERE IS YOUR CODE
});

通過程式碼來證明一下這個函式的存在

既然我們寫的程式碼都在函式內部,那我們應該通過arguments能獲取到這個函式的引數

//module-a.js
console.log('模組中的第一句程式碼');
console.log(arguments.length)

//執行結果
模組中的第一句程式碼
5

arguments.length的值是5,那八成就是這個樣子了。但感覺說服力不強,繼續看...

//module-a.js
console.log('模組中的第一句程式碼');
console.log(arguments.callee.toString())

//執行結果
模組中的第一句程式碼
function (exports, require, module, __filename, __dirname) {
    console.log('模組中的第一句程式碼');
    console.log(arguments.callee.toString())
}

終於露出了廬山真面目,為什麼可以直接用,應該一目瞭然了!(可以嘗試列印一下這個五個引數中都是什麼內容)

五、module.exports和exports

通過列印module成員,可以看到exports是module下的一個物件。module的exports屬性表示當前模組對外輸出的橋樑,module.exports指向的成員都會被暴露出去

以上示例中的寫法都是把模組內的成員掛載到module.exports中暴露出去的

const a = 10;
const add = function(a, b) {
    return a + b;
}
module.exports.a = a;
module.exports.add = add;
或者使用
module.exports={a,add}

exports又如何匯出的呢?

//module-a.js
console.log(module.exports)
console.log(exports)
//輸出結果
{}
{}

兩個成員都是物件,那會不會是同一個東西呢?

//module-a.js
console.log(module.exports)
console.log(exports)
console.log(module.exports===exports)
//輸出結果
{}
{}
true

可見兩個成員完全相等,則指向的堆記憶體的地址是同一個,所以使用exports匯出模組內的成員也是理所應當的了。

所以模組最外部函式應該是有這麼一句程式碼的

(function (exports, require, module, __filename, __dirname) {
    exports=module.exports={}; //指向同一個記憶體地址
  // HERE IS YOUR CODE
});

既然exports和module.exports是指向的是同一個記憶體,按說用法是一樣的,為什麼上邊使用exports匯出成員時,特意說明不可以使用exports直接匯出一個物件呢?不妨試一下:

//module-b.js
const a = 10;
const add = function(a, b) {
    return a + b;
}

// module.exports = { a, add };
exports = { a, add};
const moduleB = require("./module-b.js");

console.log(moduleB)//{}
console.log(moduleB.a)//undefined

實驗得出,通過exports直接匯出一個物件時在外部並拿不到匯出的資料,為什麼呢?

看一下module.exports和exports在記憶體中的情況

當載入該模組時,執行完exports=module.exports={}後的記憶體情況

當執行完exports={a,add}時的記憶體情況

當exports={a, add}時,exports在記憶體中和module.exports指向的就不是同一個記憶體地址了,說白了抱不了module.exports的大腿了,我們們上面說過,模組匯出成員是通過module.exports匯出的。exports和module.exports不是同一個記憶體時,exports自然無法匯出成員了。

既然如此那就把匯出的成員老老實實掛載到exports下吧。整洋氣一些,module.exports和exports同時使用

//module-b.js
const a = 10;
const add = function(a, b) {
    return a + b;
}

module.exports = add;
exports.a = a;
//module-a.js
const moduleB = require("./module-b.js");

console.log(moduleB)//Function
console.log(moduleB.a)//undefined

納尼???把匯出的成員掛載到exports下了為何引用的時候還是undefined???

注意:在使用exports.a=a前 使用了module.exports=add了,這時候使用exports為什麼導不出成員,大家應該都明白了【原因同上】

為了避免在匯出成員時,有這樣或那樣的問題,建議在模組中全部使用module.exports吧

六、require()

通過上面一系列的程式碼案例可以看出,require的作用是載入所依賴的檔案。說白了就是執行了所載入模組的最外層的函式。

//module-b.js
const a = 10;
console.log('module-b中列印', a)
module.exports = { a };
//module-a.js
const moduleB = require("./module-b.js");
console.log(moduleB);
執行module-a.js的結果:

module-b中列印 10
{ a: 10 }

module-b.js中的console.log執行了。可見require函式確實令模組最外部的函式執行了。

由require執行完後有個引數來接受返回值看出,模組最外部的函式執行完後是有返回值的,那麼模組最外部的函式應該是這個樣子:

(function (exports, require, module, __filename, __dirname) {
  exports = module.exports = {}; //指向同一個記憶體地址
  // HERE IS YOUR CODE
  return module.exports;//把module.exports返回出去,同時module.exports下掛載的成員也返回出去了
});

1、require載入檔案的方式(字尾名預設是js)

​ (1)通過相對路徑載入模組,以"./"或"../"開頭。比如:require("./module-b")是載入同級目錄下的module-b.js

​ (2)通過絕對路徑載入模組,以"/"開頭,這時候會去磁碟碟符根目錄或者網站根目錄去找此模組。比如:require("/module-b"),此時 會去根目錄去找module-b.js

​ (3)直接通過模組名載入,比如:require("math"),這是載入提供node內建的核心模組或者node_modules目錄中安裝的模組。當加 載的是node_modules中的模組時,查詢規則如下

//檔案所在目錄  E:/Work/Module/CommonJs/module-a.js
const math = require("math");
查詢規則:【假設每次在對應的目錄下都沒找到math模組】
1、先去CommonJS目錄下的node_modules中去查詢math模組
2、再去Module資料夾下的node_modules中去查詢math模組
3、再去Work資料夾下node_modules中去查詢math模組
4、再去E盤下的node_modules中去查詢math模組
最頂級目錄還找不到的話則報錯...

2、模組的載入機制

CommonJS模組的載入機制是,引入的值是輸出的值的拷貝,一旦模組內的值匯出後,在外部如何改變都不會影響模組內部,同樣模組內部對這個值如何改變也不會影響模組外部,猶如“嫁出去的女兒,潑出去的水”

相關文章