模組化編程是一種非常常見Javascript編程模式。它一般來說可以使得代碼更易於理解,但是有許多優秀的實踐還沒有廣為人知。在這篇文章中,我將會回顧一下js模組化編程的基礎,並且將會講到一些真的非常值得一提的進階話題,包括一個我認為是我自創的模式。
基礎
我們首先簡單地概述一下,自從三年前Eric Miraglia(YUI的開發人員)第一次發表部落格描述模組化模式以來的一些模組化模式。如果你已經對於這些模組化模式非常熟悉了,大可以直接跳過本節,從“進階模式”開始閱讀。
匿名閉包
這是一種讓一切變為可能的基本結構,同時它也是Javascript最棒的特性。我們將簡單地建立一個匿名函數並立即執行它。所有的代碼將跑在這個函數內,生存在一個提供私人化的閉包中,它足以使得這些閉包中的變數能夠貫穿我們的應用的整個生命週期。
(function () {
// ... all vars and functions are in this scope only
// still maintains access to all globals
}());
注意這對包裹匿名函數的最外層括弧。因為Javascript的語言特性,這對括弧是必須的。在js中由關鍵詞function開頭的語句總是會被認為是函式宣告式。把這段程式碼封裝裹在括弧中就可以讓解譯器知道這是個函數運算式。
全域變數匯入
Javascript有一個特性叫做隱式全域變數。無論一個變數名在哪兒被用到了,解譯器會根據範圍鏈來反向找到這個變數的var聲明語句。如果沒有找到var聲明語句,那麼這個變數就會被視為全域變數。如果這個變數用在一句指派陳述式中,同時這個變數又不存在時,就會建立出一個全域變數。這意味著在匿名閉包中使用或建立全域變數是很容易的。不幸的是,這會導致寫出的代碼極難維護,因為對於人的直觀感受來說,一眼根本分不清那些是全域的變數。
幸運的是,我們的匿名函數提供了簡單的變通方法。只要將全域變數作為參數傳遞到我們的匿名函數中,就可以得到比隱式全域變數更清晰又快速的代碼了。下面是樣本:
(function ($, YAHOO) {
// now have access to globals jQuery (as $) and YAHOO in this code
}(jQuery, YAHOO));
模組匯出
有時你不僅想要使用全域變數,你還想要聲明它們,以供反覆使用。我們可以很容易地通過匯出它們來做到這一點——通過匿名函數的傳回值。這樣做將會完成一個基本的模組化模式雛形,接下來會是一個完整的例子:
var MODULE = (function () {
var my = {},
privateVariable = 1;
function privateMethod() {
// ...
}
my.moduleProperty = 1;
my.moduleMethod = function () {
// ...
};
return my;
}());
注意我們已經聲明了一個叫做MODULE的全域模組,它擁有2個公有的屬性:一個叫做MODULE.moduleMethod的方法和一個叫做MODULE.moduleProperty的變數。另外,它還維護了一個利用匿名函數閉包的、私人的內建狀態。同時,我們可以很容易地匯入需要的全域變數,並像之前我們所學到的那樣來使用這個模組化模式。
進階模式
上面一節所描述的基礎已經足以應對許多情況,現在我們可以將這個模組化模式進一步的發展,建立更多強大的、可擴充的結構。讓我們從MODULE模組開始,一一介紹這些進階模式。
放大模式
整個模組必須在一個檔案中是模組化模式的一個限制。任何一個參與大型項目的人都會明白將js拆分多個檔案的價值。幸運的是,我們擁有一個很棒的實現來放大模組。首先,我們匯入一個模組,並為它添加屬性,最後再匯出它。下面是一個例子——從原本的MODULE中放大它:
var MODULE = (function (my) {
my.anotherMethod = function () {
// added method...
};
return my;
}(MODULE));
我們用var關鍵詞來保證一致性,雖然它在此處不是必須的。在這段代碼執行完之後,我們的模組就已經擁有了一個新的、叫做MODULE.anotherMethod的公有方法。這個放大檔案也會維護它自己的私人內建狀態和匯入的對象。
寬放大模式
我們的上面例子需要我們的初始化模組最先被執行,然後放大模組才能執行,當然有時這可能也不一定是必需的。Javascript應用可以做到的、用來提升效能的、最棒的事之一就是非同步執行指令碼。我們可以建立靈活的多部分模組並通過寬放大模式使它們可以以任意順序載入。每一個檔案都需要按下面的結構組織:
var MODULE = (function (my) {
// add capabilities...
return my;
}(MODULE {}));
在這個模式中,var運算式使必需的。注意如果MODULE還未初始化過,這句匯入語句會建立MODULE。這意味著你可以用一個像LABjs的工具來並行載入你所有的模組檔案,而不會被阻塞。
緊放大模式
寬放大模式非常不錯,但它也會給你的模組帶來一些限制。最重要的是,你不能安全地覆蓋模組的屬性。你也無法在初始化的時候,使用其他檔案中的屬性(但你可以在啟動並執行時候用)。緊放大模式包含了一個載入的順序序列,並且允許覆蓋屬性。這兒是一個簡單的例子(放大我們的原始MODULE):
var MODULE = (function (my) {
var old_moduleMethod = my.moduleMethod;
my.moduleMethod = function () {
// method override, has access to old through old_moduleMethod...
};
return my;
}(MODULE));
我們在上面的例子中覆蓋了MODULE.moduleMethod的實現,但在需要的時候,可以維護一個對原來方法的引用。
複製與繼承
var MODULE_TWO = (function (old) {
var my = {},
key;
for (key in old) {
if (old.hasOwnProperty(key)) {
my[key] = old[key];
}
}
var super_moduleMethod = old.moduleMethod;
my.moduleMethod = function () {
// override method on the clone, access to super through super_moduleMethod
};
return my;
}(MODULE));
這個模式可能是最缺乏靈活性的一種選擇了。它確實使得代碼顯得很整潔,但那是用靈活性的代價換來的。正如我上面寫的這段代碼,如果某個屬性是對象或者函數,它將不會被複製,而是會成為這個對象或函數的第二個引用。修改了其中的某一個就會同時修改另一個(譯者註:因為它們根本就是一個啊!)。這可以通過遞迴複製過程來解決這個對象複製問題,但函數複製可能無法解決,也許用eval可以解決吧。因此,我在這篇文章中講述這個方法僅僅是考慮到文章的完整性。
跨檔案私人變數
把一個模組分到多個檔案中有一個重大的限制:每一個檔案都維護了各自的私人變數,並且無法訪問到其他檔案的私人變數。但這個問題是可以解決的。這裡有一個維護跨檔案私人變數的、寬放大模組的例子:
var MODULE = (function (my) {
var _private = my._private = my._private {},
_seal = my._seal = my._seal function () {
delete my._private;
delete my._seal;
delete my._unseal;
},
_unseal = my._unseal = my._unseal function () {
my._private = _private;
my._seal = _seal;
my._unseal = _unseal;
};
// permanent access to _private, _seal, and _unseal
return my;
}(MODULE {}));
所有檔案可以在它們各自的_private變數上設定屬性,並且它理解可以被其他檔案訪問。一旦這個模組載入完成,應用程式可以調用MODULE._seal()來防止外部對內部_private的調用。如果這個模組需要被重新放大,在任何一個檔案中的內部方法可以在載入新的檔案前調用_unseal(),並在新檔案執行好以後再次調用_seal()。我如今在工作中使用這種模式,而且我在其他地方還沒有見過這種方法。我覺得這是一種非常有用的模式,很值得就這個模式本身寫一篇文章。
子模組
我們的最後一種進階模式是顯而易見最簡單的。建立子模組有許多優秀的執行個體。這就像是建立一般的模組一樣:
MODULE.sub = (function () {
var my = {};
// ...
return my;
}());
雖然這看上去很簡單,但我覺得還是值得在這裡提一提。子模組擁有一切一般模組的進階優勢,包括了放大模式和私人化狀態。
結論
大多數進階模式可以結合到一起來建立一個更為有用的模式。如果實在要我推薦一種設計複雜應用程式的模組化模式的化,我會選擇結合寬放大模式、私人變數和子模組。
我還未考慮過這些模式的效能問題,但我寧願把這轉化為一個更簡單的思考方式:如果一個模組化模式有很好的效能,那麼它能夠把最小化做的很棒,使得下載這個指令檔更快。使用寬放大模式可以允許簡單的非阻塞並行下載,這就會加快下載速度。初始化時間可能會稍慢於其他方法,但權衡利弊後這還是值得的。只要全域變數匯入準確,運行時效能應該會不會受到影響,而且還有可能在子模組中通過用私人變數縮短引用鏈來得到更快的運行速度。
作為結束,這裡是一個子模組動態地把自身載入到它的父模組的例子(如果父模組不存在則建立它)。為了簡潔,我把私人變數給去除了,當然加上私人變數也是很簡單的啦。這種編程模式允許一整個複雜層級結構程式碼程式庫通過子模組並行地完成載入。
var UTIL = (function (parent, $) {
var my = parent.ajax = parent.ajax {};
my.get = function (url, params, callback) {
// ok, so I'm cheating a bit :)
return $.getJSON(url, params, callback);
};
// etc...
return parent;
}(UTIL {}, jQuery));
我希望這篇文章對你有協助,請在文章下面留言分享你的想法。從現在起,就開始寫更棒、更模組化的Javascript吧!