前端模塊化之CommonJS

一、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模塊的加載機制是,引入的值是輸出的值的拷貝,一旦模塊內的值導出後,在外部如何改變都不會影響模塊內部,同樣模塊內部對這個值如何改變也不會影響模塊外部,猶如「嫁出去的女兒,潑出去的水」