本文將爲你們透徹的介紹關於Node的模塊化——CommonJS的一切。html
看完本文能夠掌握,如下幾個方面:前端
module.exports
是如何導出變量的,值類型和引用類型導出間的差別;module.exports
和exports
又有怎樣的區別和聯繫;PS:本篇文章爲「Node.js系列」的第二篇,完全搞懂JS和Node中的模塊化機制;
以後會保持每週1~2篇的Node.js文章,歡迎你們和我一塊兒學習大前端進階系列。node
在不少開發的狀況下,咱們都知道要使用模塊化開發,那爲何要使用它呢?webpack
而事實上,模塊化開發
最終的目的是將程序劃分紅一個個小的結構
;web
本身的邏輯代碼
,有本身的做用域
,不會影響到其餘的結構;變量
、函數
、對象
等導出給其結構使用;變量
、函數
、對象
等;上面說提到的結構
,就是模塊
;面試
按照這種結構
劃分開發程序的過程,就是模塊化開發
的過程;ajax
在網頁開發的早期,因爲JavaScript僅僅做爲一種腳本語言
,只能作一些簡單的表單驗證或動畫實現等,它仍是具備不少的缺陷問題的,好比:算法
但隨着前端和JavaScript的快速發展,JavaScript代碼變得愈來愈複雜了;編程
先後端開發分離
,意味着後端返回數據後,咱們須要經過JavaScript進行前端頁面的渲染
;前端路由
、狀態管理
等等一系列複雜的需求須要經過JavaScript來實現;複雜的後端程序
,沒有模塊化是致命的硬傷;因此,模塊化已是JavaScript一個很是迫切的需求:後端
可是JavaScript自己,直到ES6
(2015)才推出了本身的模塊化方案;
在此以前,爲了讓JavaScript支持模塊化,涌現出了不少不一樣的模塊化規範:AMD、CMD、CommonJS
等;
到此,咱們明白了爲何要用模塊化開發?
那若是沒有模塊化會帶來什麼問題呢?
當咱們在公司面對一個大型的前端項目時,一般是多人開發的,會把不一樣的業務邏輯分步在多個文件夾當中。
小豪開發的bar.js
文件
var name = "小豪";
console.log("bar.js----", name);
複製代碼
小豪開發的baz.js
文件
console.log("baz.js----", name);
複製代碼
小紅開發的foo.js
文件
var name = "小紅";
console.log("foo.js----", name);
複製代碼
引用路徑以下:
<body>
<script src="./bar.js"></script>
<script src="./foo.js"></script>
<script src="./baz.js"></script>
</body>
複製代碼
最後當我去執行的時候,卻發現執行結果:
當咱們看到這個結果,有的小夥伴可能就會驚訝,baz.js
文件不是小豪寫的麼?爲何會輸出小紅的名字呢?
究其緣由,咱們才發現,其實JavaScript
是沒有模塊化的概念(至少到如今爲止尚未用到ES6規範),換句話說就是每一個.js
文件並非一個獨立的模塊,沒有本身的做用域
,因此在.js
文件中定義的變量,都是能夠被其餘的地方共享的,因此小豪開發的baz.js
裏面的name,其實訪問的是小紅從新聲明的。
可是共享也有一點很差就是,項目的其餘協做人員也能夠隨意的改變它們,顯然這不是咱們想要的。
因此,隨着前端的發展,模塊化變得必不可少,那麼在早期是如何解決的呢?
在早期,由於函數是有本身的做用域,因此能夠採用當即函數調用表達式(IIFE),也就是自執行函數,把要供外界使用的變量做爲函數的返回結果。
小豪——bar.js
var moduleBar = (function () {
var name = "小豪";
var age = "18";
console.log("bar.js----", name, age);
return {
name,
age,
};
})();
複製代碼
小豪——baz.js
console.log("baz.js----", moduleBar.name);
console.log("baz.js----", moduleBar.age);
複製代碼
小紅——foo.js
(function () {
var name = "小紅";
var age = 20;
console.log("foo.js----", name, age);
})();
複製代碼
來看一下,解決以後的輸出結果,原調用順序不變;
可是,這又帶來了新的問題:
模塊中返回對象的命名
,才能在其餘模塊使用過程當中正確的使用;雜亂無章
,每一個文件中的代碼都須要包裹在一個匿名函數中來編寫;沒有合適的規範
狀況下,每一個人、每一個公司均可能會任意命名、甚至出現模塊名稱相同的狀況;因此如今急需一個統一的規範,來解決這些缺陷問題,就此CommonJS規範
問世了。
CommonJS是一個規範,最初提出來是在瀏覽器之外的地方使用,而且當時被命名爲ServerJS,後來爲了體現它的普遍性,修改成CommonJS規範。
Node是CommonJS在服務器端一個具備表明性的實現;
Browserify是CommonJS在瀏覽器中的一種實現;
webpack打包工具具有對CommonJS的支持和轉換;
正是由於Node中對CommonJS
進行了支持和實現,因此它具有如下幾個特色;
每個js文件都是一個單獨的模塊
;CommonJS規範的核心變量
: exports、module.exports、require;模塊化
開發;無疑,模塊化的核心是導出和導入,Node中對其進行了實現:
對模塊中的內容進行導出
;導入其餘模塊(自定義模塊、系統模塊、第三方庫模塊)中的內容
;假設如今有兩個文件:
bar.js
const name = "時光屋小豪";
const age = 18;
function sayHello(name) {
console.log("hello" + name);
}
複製代碼
main.js
console.log(name);
console.log(age);
複製代碼
執行node main.js以後,會看到
這是由於在當前main.js
模塊內,沒有發現name
這個變量;
這點與咱們前面看到的明顯不一樣,由於Node中每一個js文件都是一個單獨的模塊。
那麼若是要在別的文件內訪問bar.js
變量
bar.js
須要導出本身想要暴露的變量、函數、對象等等;main.js
從bar.js
引入想用的變量、函數、對象等等;exports是一個對象,咱們能夠在這個對象中添加不少個屬性,添加的屬性會導出。
bar.js
文件導出:
const name = "時光屋小豪";
const age = 18;
function sayHello(name) {
console.log("hello" + name);
}
exports.name = name;
exports.age = age;
exports.sayHello = sayHello;
複製代碼
main.js
文件導入:
const bar = require('./bar');
console.log(bar.name); // 時光屋小豪
console.log(bar.age); // 18
複製代碼
其中要注意的點:
bar
變量等於exports
對象;bar = exports
複製代碼
因此咱們經過bar.xxx
來使用導出文件內的變量,好比name,age;
require
實際上是一個函數
,返回值是一個對象,值爲「導出文件」的exports
對象;
在Node中,有一個特殊的全局對象,其實exports
就是其中之一。
若是在文件內,再也不使用exports.xxx
的形式導出某個變量的話,其實exports
就是一個空對象。
模塊之間的引用關係
main.js
中require導入的時候,它會去自動查找特殊的全局對象exports
,而且把require
函數的執行結果賦值給bar
;bar
和exports
指向同一個引用(引用地址相同);exports
上有變量,則會放到bar
對象上,正由於這樣咱們才能從bar
上讀取想用的變量;爲了進一步論證,bar
和exports
是同一個對象:
咱們加入定時器看看
因此綜上所述,Node
中實現CommonJS規範
的本質就是對象的引用賦值
(淺拷貝本質)。
把exports
對象的引用賦值bar
對象上。
CommonJS規範的本質就是對象的引用賦值
可是Node中咱們常用module.exports
導出東西,也會遇到這樣的面試題:
module.exports
和exports
有什麼關係或者區別呢?
require本質就是一個函數,能夠幫助咱們引入一個文件(模塊)中導入的對象。
require的查找規則nodejs.org/dist/latest…
結論一: 模塊在被第一次引入時,模塊中的js代碼會被運行一次
// aaa.js
const name = 'coderwhy';
console.log("Hello aaa");
setTimeout(() => {
console.log("setTimeout");
}, 1000);
複製代碼
// main.js
const aaa = require('./aaa');
複製代碼
aaa.js中的代碼在引入時會被運行一次
結論二:模塊被屢次引入時,會緩存,最終只加載(運行)一次
// main.js
const aaa = require('./aaa');
const bbb = require('./bbb');
複製代碼
/// aaa.js
const ccc = require("./ccc");
複製代碼
// bbb.js
const ccc = require("./ccc");
複製代碼
// ccc.js
console.log('ccc被加載');
複製代碼
ccc中的代碼只會運行一次。
爲何只會加載運行一次呢?
結論三:若是有循環引入,那麼加載順序是什麼?
若是出現下面模塊的引用關係,那麼加載順序是什麼呢?
如下是經過維基百科對CommonJS規範的解析:
CommonJS中是沒有module.exports的概念的;
可是爲了實現模塊的導出,Node中使用的是Module
的類,每個模塊都是Module
的一個實例module
;
因此在Node中真正用於導出的其實根本不是exports
,而是module.exports
;
exports
只是module
上的一個對象
可是,爲何exports也能夠導出呢?
這是由於module
對象的exports
屬性是exports
對象的一個引用;
等價於module.exports = exports = main中的bar
(CommonJS內部封裝);
聯繫:module.exports = exports
進一步論證module.exports = exports
// bar.js
const name = "時光屋小豪";
exports.name = name;
setTimeout(() => {
module.exports.name = "哈哈哈";
console.log("bar.js中1s以後", exports.name);
}, 1000);
複製代碼
// main.js
const bar = require("./bar");
console.log("main.js", bar.name);
setTimeout((_) => {
console.log("main.js中1s以後", bar.name);
}, 2000);
複製代碼
在上面代碼中,只要在bar.js
中修改exports
對象裏的屬性,導出的結果都會變,由於即便真正導出的是 module.exports
,而module.exports
和exports
是都是相同的引用地址,改變了其中一個的屬性,另外一個也會跟着改變。
注意:真正導出的模塊內容的核心實際上是module.exports,只是爲了實現CommonJS的規範,恰好module.exports對exports對象使用的是同一個引用而已
區別:有如下兩點
那麼若是,代碼這樣修改了:
module.exports
也就和 exports
沒有任何關係了;
exports
怎麼改,都不會影響最終的導出結果;由於module.exports = { xxx }
這樣的形式,會在堆內存中新開闢出一塊內存空間,會生成一個新的對象,用它取代以前的exports
對象的導出
require
導入的對象是新的對象;講完它們兩個的區別,來看下面這兩個例子,看看本身是否真正掌握了module.exports
的用法
練習1:導出的變量爲值類型
// bar.js
let name = "時光屋小豪";
setTimeout(() => {
name = "123123";
}, 1000);
module.exports = {
name: name,
age: "20",
sayHello: function (name) {
console.log("你好" + name);
},
};
複製代碼
// main.js
const bar = require("./bar");
console.log("main.js", bar.name); // main.js 時光屋小豪
setTimeout(() => {
console.log("main.js中2s後", bar.name); // main.js中2s後 時光屋小豪
}, 2000);
複製代碼
練習2:導出的變量爲引用類型
// bar.js
let info = {
name: "時光屋小豪",
};
setTimeout(() => {
info.name = "123123";
}, 1000);
module.exports = {
info: info,
age: "20",
sayHello: function (name) {
console.log("你好" + name);
},
};
複製代碼
// main.js
const bar = require("./bar");
console.log("main.js", bar.info.name); // main.js 時光屋小豪
setTimeout(() => {
console.log("main.js中2s後", bar.info.name); // main.js中2s後 123123
}, 2000);
複製代碼
從main.js
輸出結果來看,定時器修改的name
變量的結果,並無影響main.js
中導入的結果。
module.exports
的內存裏(練1)module.exports
裏存放的是info的引用地址,因此由定時器更改的變量,會影響main.js
導入的結果(練2)CommonJS模塊加載js文件的過程是運行時加載的,而且是同步的:
const flag = true;
if (flag) {
const foo = require('./foo');
console.log("等require函數執行完畢後,再輸出這句代碼");
}
複製代碼
CommonJS經過module.exports導出的是一個對象:
CommonJS規範的本質就是對象的引用賦值
《JavaScript模塊化——ES Module》
在下一篇文章中,
若是你以爲這篇內容對你挺有啓發,我想邀請你幫我三個小忙: