模塊

學習于廖雪峰的官方網站

在計算機程序的開發過程中,隨著程序代碼越寫越多,在一個文件里代碼就會越來越長,越來越不容易維護。

為了編寫可維護的代碼,我們把很多函數分組,分別放到不同的文件里,這樣,每個文件包含的代碼就相對較少,很多編程語言都采用這種組織代碼的方式。在Node環境中,一個.js文件就稱之為一個模塊(module)。

使用模塊有什么好處?

最大的好處是大大提高了代碼的可維護性。其次,編寫代碼不必從零開始。當一個模塊編寫完畢,就可以被其他地方引用。我們在編寫程序的時候,也經常引用其他模塊,包括Node內置的模塊和來自第三方的模塊。

使用模塊還可以避免函數名和變量名沖突。相同名字的函數和變量完全可以分別存在不同的模塊中,因此,我們自己在編寫模塊時,不必考慮名字會與其他模塊沖突。

在上一節,我們編寫了一個hello.js文件,這個hello.js文件就是一個模塊,模塊的名字就是文件名(去掉.js后綴),所以hello.js文件就是名為hello的模塊。

我們把hello.js改造一下,創建一個函數,這樣我們就可以在其他地方調用這個函數:

'use strict';

var s = 'Hello';

function greet(name) {
    console.log(s + ', ' + name + '!');
}

module.exports = greet;

函數greet()是我們在hello模塊中定義的,你可能注意到最后一行是一個奇怪的賦值語句,它的意思是,把函數greet作為模塊的輸出暴露出去,這樣其他模塊就可以使用greet函數了。

問題是其他模塊怎么使用hello模塊的這個greet函數呢?我們再編寫一個main.js文件,調用hello模塊的greet函數:

'use strict';

// 引入hello模塊:
var greet = require('./hello');

var s = 'Michael';

greet(s); // Hello, Michael!

注意到引入hello模塊用Node提供的require函數:

var greet = require('./hello');

引入的模塊作為變量保存在greet變量中,那greet變量到底是什么東西?其實變量greet就是在hello.js中我們用module.exports = greet;輸出的greet函數。所以,main.js就成功地引用了hello.js模塊中定義的greet()函數,接下來就可以直接使用它了。

在使用require()引入模塊的時候,請注意模塊的相對路徑。因為main.js和hello.js位于同一個目錄,所以我們用了當前目錄.:

var greet = require('./hello'); // 不要忘了寫相對目錄!

如果只寫模塊名:

var greet = require('hello');

則Node會依次在內置模塊、全局模塊和當前模塊下查找hello.js,你很可能會得到一個錯誤:

module.js
    throw err;
          ^
Error: Cannot find module 'hello'
    at Function.Module._resolveFilename
    at Function.Module._load
    ...
    at Function.Module._load
    at Function.Module.runMain

遇到這個錯誤,你要檢查:

  • 模塊名是否寫對了;
  • 模塊文件是否存在;
  • 相對路徑是否寫對了。

CommonJS規范

這種模塊加載機制被稱為CommonJS規范。在這個規范下,每個.js文件都是一個模塊,它們內部各自使用的變量名和函數名都互不沖突,例如,hello.js和main.js都申明了全局變量var s = 'xxx',但互不影響。

一個模塊想要對外暴露變量(函數也是變量),可以用module.exports = variable;,一個模塊要引用其他模塊暴露的變量,用var ref = require('module_name');就拿到了引用模塊的變量。

Node模塊的原理

當我們編寫JavaScript代碼時,我們可以申明全局變量:

var s = 'global';

在瀏覽器中,大量使用全局變量可不好。如果你在a.js中使用了全局變量s,那么,在b.js中也使用全局變量s,將造成沖突,b.js中對s賦值會改變a.js的運行邏輯。

也就是說,JavaScript語言本身并沒有一種模塊機制來保證不同模塊可以使用相同的變量名。

那Node.js是如何實現這一點的?

其實要實現“模塊”這個功能,并不需要語法層面的支持。Node.js也并不會增加任何JavaScript語法。實現“模塊”功能的奧妙就在于JavaScript是一種函數式編程語言,它支持閉包。如果我們把一段JavaScript代碼用一個函數包裝起來,這段代碼的所有“全局”變量就變成了函數內部的局部變量。

請注意我們編寫的hello.js代碼是這樣的:

var s = 'Hello';
var name = 'world';

console.log(s + ' ' + name + '!');

Node.js加載了hello.js后,它可以把代碼包裝一下,變成這樣執行:

(function () {
    // 讀取的hello.js代碼:
    var s = 'Hello';
    var name = 'world';

    console.log(s + ' ' + name + '!');
    // hello.js代碼結束
})();

這樣一來,原來的全局變量s現在變成了匿名函數內部的局部變量。如果Node.js繼續加載其他模塊,這些模塊中定義的“全局”變量s也互不干擾。

所以,Node利用JavaScript的函數式編程的特性,輕而易舉地實現了模塊的隔離。

但是,模塊的輸出module.exports怎么實現?

這個也很容易實現,Node可以先準備一個對象module:

// 準備module對象:
var module = {
    id: 'hello',
    exports: {}
};
var load = function (module) {
    // 讀取的hello.js代碼:
    function greet(name) {
        console.log('Hello, ' + name + '!');
    }
    
    module.exports = greet;
    // hello.js代碼結束
    return module.exports;
};
var exported = load(module);
// 保存module:
save(module, exported);

可見,變量module是Node在加載js文件前準備的一個變量,并將其傳入加載函數,我們在hello.js中可以直接使用變量module原因就在于它實際上是函數的一個參數:

module.exports = greet;

通過把參數module傳遞給load()函數,hello.js就順利地把一個變量傳遞給了Node執行環境,Node會把module變量保存到某個地方。

由于Node保存了所有導入的module,當我們用require()獲取module時,Node找到對應的module,把這個module的exports變量返回,這樣,另一個模塊就順利拿到了模塊的輸出:

var greet = require('./hello');

以上是Node實現JavaScript模塊的一個簡單的原理介紹。

module.exports vs exports

很多時候,你會看到,在Node環境中,有兩種方法可以在一個模塊中輸出變量:

  • 方法一:對module.exports賦值:
// hello.js

function hello() {
    console.log('Hello, world!');
}

function greet(name) {
    console.log('Hello, ' + name + '!');
}

module.exports = {
    hello: hello,
    greet: greet
};
  • 方法二:直接使用exports:
// hello.js

function hello() {
    console.log('Hello, world!');
}

function greet(name) {
    console.log('Hello, ' + name + '!');
}

function hello() {
    console.log('Hello, world!');
}

exports.hello = hello;
exports.greet = greet;

但是你不可以直接對exports賦值:

// 代碼可以執行,但是模塊并沒有輸出任何變量:
exports = {
    hello: hello,
    greet: greet
};

如果你對上面的寫法感到十分困惑,不要著急,我們來分析Node的加載機制:

首先,Node會把整個待加載的hello.js文件放入一個包裝函數load中執行。在執行這個load()函數前,Node準備好了module變量:

var module = {
    id: 'hello',
    exports: {}
};

load()函數最終返回module.exports:

var load = function (exports, module) {
    // hello.js的文件內容
    ...
    // load函數返回:
    return module.exports;
};

var exported = load(module.exports, module);

也就是說,默認情況下,Node準備的exports變量和module.exports變量實際上是同一個變量,并且初始化為空對象{},于是,我們可以寫:

exports.foo = function () { return 'foo'; };
exports.bar = function () { return 'bar'; };

也可以寫:

module.exports.foo = function () { return 'foo'; };
module.exports.bar = function () { return 'bar'; };

換句話說,Node默認給你準備了一個空對象{},這樣你可以直接往里面加東西。

但是,如果我們要輸出的是一個函數或數組,那么,只能給module.exports賦值:

module.exports = function () { return 'foo'; };

給exports賦值是無效的,因為賦值后,module.exports仍然是空對象{}。

總結

  • 在Node環境中,一個.js文件就稱之為一個模塊(module)。
  • module大大提高了代碼的可維護性;可以被其他地方引用;使用模塊還可以避免函數名和變量名沖突
  • 要在模塊中對外輸出變量,用:
module.exports = variable;

輸出的變量可以是任意對象、函數、數組等等。

  • 引入其他模塊輸出的對象,用:
var foo = require('other_module');

引入的對象具體是什么,取決于引入模塊輸出的對象。

  • 兩種方法輸出變量
//第一種
module.exports = {
    hello: hello,
    greet: greet
};
//第二種
exports.hello = hello;
exports.greet = greet;
  • 直接對module.exports賦值,可以應對任何情況。
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,563評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,694評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,672評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,965評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,690評論 6 413
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,019評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,013評論 3 449
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,188評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,718評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,438評論 3 360
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,667評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,149評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,845評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,252評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,590評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,384評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,635評論 2 380

推薦閱讀更多精彩內容

  • 模塊通常是指編程語言所提供的代碼組織機制,利用此機制可將程序拆解為獨立且通用的代碼單元。所謂模塊化主要是解決代碼分...
    MapleLeafFall閱讀 1,180評論 0 0
  • 模塊 Node 有簡單的模塊加載系統。在 Node 里,文件和模塊是一一對應的。下面例子里,foo.js加載同一個...
    保川閱讀 608評論 0 0
  • 為了編寫可維護的代碼,我們把很多函數分組,分別放到不同的文件里,這樣,每個文件包含的代碼就相對較少,很多編...
    想當一個大頭兵閱讀 1,391評論 0 0
  • 模塊 在計算機程序的開發中,隨著代碼越寫越多,在一個文件里代碼就會越來越長,越來越不容易維護.為了編寫可維護的代碼...
    _我和你一樣閱讀 247評論 0 0
  • 模塊 在計算機程序的開發中,隨著代碼越寫越多,在一個文件里代碼就會越來越長,越來越不容易維護.為了編寫可維護的代碼...
    _我和你一樣閱讀 179評論 0 0