模塊

學(xué)習(xí)于廖雪峰的官方網(wǎng)站

在計(jì)算機(jī)程序的開發(fā)過程中,隨著程序代碼越寫越多,在一個(gè)文件里代碼就會(huì)越來越長,越來越不容易維護(hù)。

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

使用模塊有什么好處?

最大的好處是大大提高了代碼的可維護(hù)性。其次,編寫代碼不必從零開始。當(dāng)一個(gè)模塊編寫完畢,就可以被其他地方引用。我們?cè)诰帉懗绦虻臅r(shí)候,也經(jīng)常引用其他模塊,包括Node內(nèi)置的模塊和來自第三方的模塊。

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

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

我們把hello.js改造一下,創(chuàng)建一個(gè)函數(shù),這樣我們就可以在其他地方調(diào)用這個(gè)函數(shù):

'use strict';

var s = 'Hello';

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

module.exports = greet;

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

問題是其他模塊怎么使用hello模塊的這個(gè)greet函數(shù)呢?我們?cè)倬帉懸粋€(gè)main.js文件,調(diào)用hello模塊的greet函數(shù):

'use strict';

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

var s = 'Michael';

greet(s); // Hello, Michael!

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

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

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

在使用require()引入模塊的時(shí)候,請(qǐng)注意模塊的相對(duì)路徑。因?yàn)閙ain.js和hello.js位于同一個(gè)目錄,所以我們用了當(dāng)前目錄.:

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

如果只寫模塊名:

var greet = require('hello');

則Node會(huì)依次在內(nèi)置模塊、全局模塊和當(dāng)前模塊下查找hello.js,你很可能會(huì)得到一個(gè)錯(cuò)誤:

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

遇到這個(gè)錯(cuò)誤,你要檢查:

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

CommonJS規(guī)范

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

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

Node模塊的原理

當(dāng)我們編寫JavaScript代碼時(shí),我們可以申明全局變量:

var s = 'global';

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

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

那Node.js是如何實(shí)現(xiàn)這一點(diǎn)的?

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

請(qǐng)注意我們編寫的hello.js代碼是這樣的:

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

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

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

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

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

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

所以,Node利用JavaScript的函數(shù)式編程的特性,輕而易舉地實(shí)現(xiàn)了模塊的隔離。

但是,模塊的輸出module.exports怎么實(shí)現(xiàn)?

這個(gè)也很容易實(shí)現(xiàn),Node可以先準(zhǔn)備一個(gè)對(duì)象module:

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

可見,變量module是Node在加載js文件前準(zhǔn)備的一個(gè)變量,并將其傳入加載函數(shù),我們?cè)趆ello.js中可以直接使用變量module原因就在于它實(shí)際上是函數(shù)的一個(gè)參數(shù):

module.exports = greet;

通過把參數(shù)module傳遞給load()函數(shù),hello.js就順利地把一個(gè)變量傳遞給了Node執(zhí)行環(huán)境,Node會(huì)把module變量保存到某個(gè)地方。

由于Node保存了所有導(dǎo)入的module,當(dāng)我們用require()獲取module時(shí),Node找到對(duì)應(yīng)的module,把這個(gè)module的exports變量返回,這樣,另一個(gè)模塊就順利拿到了模塊的輸出:

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

以上是Node實(shí)現(xiàn)JavaScript模塊的一個(gè)簡單的原理介紹。

module.exports vs exports

很多時(shí)候,你會(huì)看到,在Node環(huán)境中,有兩種方法可以在一個(gè)模塊中輸出變量:

  • 方法一:對(duì)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;

但是你不可以直接對(duì)exports賦值:

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

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

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

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

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

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

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

也就是說,默認(rèn)情況下,Node準(zhǔn)備的exports變量和module.exports變量實(shí)際上是同一個(gè)變量,并且初始化為空對(duì)象{},于是,我們可以寫:

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

也可以寫:

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

換句話說,Node默認(rèn)給你準(zhǔn)備了一個(gè)空對(duì)象{},這樣你可以直接往里面加?xùn)|西。

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

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

給exports賦值是無效的,因?yàn)橘x值后,module.exports仍然是空對(duì)象{}。

總結(jié)

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

輸出的變量可以是任意對(duì)象、函數(shù)、數(shù)組等等。

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

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

  • 兩種方法輸出變量
//第一種
module.exports = {
    hello: hello,
    greet: greet
};
//第二種
exports.hello = hello;
exports.greet = greet;
  • 直接對(duì)module.exports賦值,可以應(yīng)對(duì)任何情況。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 模塊通常是指編程語言所提供的代碼組織機(jī)制,利用此機(jī)制可將程序拆解為獨(dú)立且通用的代碼單元。所謂模塊化主要是解決代碼分...
    MapleLeafFall閱讀 1,256評(píng)論 0 0
  • 模塊 Node 有簡單的模塊加載系統(tǒng)。在 Node 里,文件和模塊是一一對(duì)應(yīng)的。下面例子里,foo.js加載同一個(gè)...
    保川閱讀 691評(píng)論 0 0
  • 為了編寫可維護(hù)的代碼,我們把很多函數(shù)分組,分別放到不同的文件里,這樣,每個(gè)文件包含的代碼就相對(duì)較少,很多編...
    想當(dāng)一個(gè)大頭兵閱讀 1,459評(píng)論 0 0
  • 模塊 在計(jì)算機(jī)程序的開發(fā)中,隨著代碼越寫越多,在一個(gè)文件里代碼就會(huì)越來越長,越來越不容易維護(hù).為了編寫可維護(hù)的代碼...
    _我和你一樣閱讀 267評(píng)論 0 0
  • 模塊 在計(jì)算機(jī)程序的開發(fā)中,隨著代碼越寫越多,在一個(gè)文件里代碼就會(huì)越來越長,越來越不容易維護(hù).為了編寫可維護(hù)的代碼...
    _我和你一樣閱讀 223評(píng)論 0 0

友情鏈接更多精彩內(nèi)容