<譯自http://www.adequatelygood.com/2010/3/JavaScript-Module-Pattern-In-Depth>
<譯者按:一個具有一定複雜度的應用程序必須是模塊化的。Javascript語言本身具有一些不適合模塊化的特性,比如過多依賴全局變量,沒有名字空間,基於原型的繼承等等,這些給Javascript的模塊化設計帶來一定難度。這篇文章通過對一些流行框架的設計模式進行解析,規納出一些重要的模塊設計模式。無論是對我們閱讀流行的Javascript框架,還是在不使用Javascript庫的情況下編寫自己的模塊乃至於框架,都將有所助益。>
模塊設計模式是常見的Javascript設計模式。這些模式基本上比較容易理解,但仍有一些高級用法並沒有引起廣泛重視。在這篇文章裏,我將討論模塊設計模式的基礎,以及一些真正值得重視的高級話題,其中有一些見解我自認爲是十分獨到的。
基本模式
讓我們先簡單回顧一些基本的設計模式,這些設計模式三年前首次被Eric Miraglia (YUI開發人員)發表在他的博客中。如果你對這些模式比較熟悉,可以跳過直接去看高級設計模式。
匿名閉包
這是一切功能得以實現的基礎結構,也是Javascript最好的特性。簡單地創建一個匿名函數並且立即執行它。所有在函數中執行的代碼生存在一個閉包(closure)之中,閉包在應用程序的全生命期提供了這些代碼的私有性(privacy)和狀態。
(function () {
// … all vars and functions are in this scope only
// still maintain access to all globals
}());
注意匿名函數之外的一對括號。這是由Javascript的語言特性所要求的。在Javascript中,由關鍵字function開頭的語句總是被認爲是函數聲明,將其用一對括號括起來就生成了一個函數表達式。
導入全局變量
Javascript有一個稱作隱式全局作用域的特性。當一個變量名被使用時,解析器遍歷作用域鏈以查找關於這個變量的var聲明語句。如果找不到,則這個變量就認爲是全局變量。如果是在一個賦值語句中被使用,且該變量此前不存在,則一個新的全局變量被創建。這意味着很容易在閉包中使用/創建一個全局變量。不幸地是,這會導致難於維護的代碼,因爲對人來說,很難發現指定文件中哪些變量是全局變量。
幸運地是,匿名函數提供了一個容易的變通方法。通過將全局變量作爲參數傳入匿名函數,我們把它們導入到函數內部,由此代碼既清晰,又快速(查找本地變量比全局變量要快)。這裏有一個例子:
(function ($, YAHOO) {
// now have access to globals jQuery (as $) and YAHOO in this code
}(jQuery, YAHOO));
模塊導出
有時候你不只是想使用全局變量,而且想聲明一個全局變量。我們可以通過匿名函數的return value輕易地導出它們。 如此一來我們就完成了最基礎的模塊設計模式,完整示例如下:
var MODULE = (function () {
var my = {},
privateVariable = 1;
function privateMethod() {
// ...
}
my.moduleProperty = 1;
my.moduleMethod = function () {
// ...
};
return my;
}());
注意這裏我們聲明瞭一個名爲MODULE的全局模塊名字,幷包含兩個公共屬性:一個名爲MODULE.moduleMethod的方法和一個名爲MODULE.moduleProperty的變量。此外,它通過閉包維護了一個私有的內部狀態。同時,通過使用前面學到的模式,我們也可以輕鬆地導入全局變量。
高級模式
儘管上面的模式已堪對付大多數應用,但我們還可以在此基礎上更進一步,創造一些更強大,更易擴展的結構。讓我們從已定義的模塊MODULE開始,逐一討論。
擴張模式(Augmentation)
前述模式的缺陷之一是,整個模塊必須定義在一個文件裏。那些在較大代碼量工程工作過的人都能理解將代碼分散到多個文件的價值。幸運地是,我們有了一個擴張模塊的很好的解決辦法。首先,我們導入一個模塊,然後增加新屬性,然後再導出它們。請看例子:
var MODULE = (function (my) {
my.anotherMethod = function () {
// added method...
};
return my;
}(MODULE));
鬆耦合擴張 (Loose Augmentation)
前面講到的代碼依賴於初始化模塊必須首先創建,然後擴張才能進行。可是這並不總是成立。Javascript提升性能的方法之一就是異步加載腳本。通過鬆耦合擴張,我們就可以將多塊的腳本以任意次序載入。每個文件都應該有着如下的結構:
var MODULE = (function (my) {
// add capabilities...
return my;
}(MODULE || {}));
在此模式中,var聲明始終是必須的。注意導入部分總是會創建模塊,如果事前不存在的話。這意味着你可以使用類似LABjs的工具併發地載入你所有的模塊文件,而不是阻塞式地依次載入。
緊耦合擴張 (Tight Augmentation)
鬆耦合擴張很不錯,但它也給你的模塊帶來一些限制。最重要地是,你無法安全地重載模塊的屬性,同時,你也不能在初始化期間使用定義在其它文件中的模塊屬性(但你可以在初始化完成後這樣做)。緊耦合擴張依然強調加載次序,但允許重載。下面是一個簡單的例子:
var MODULE = (function (my) {
var old_moduleMethod = my.moduleMethod;
my.moduleMethod = function () {
// method override, has access to old through old_moduleMethod...
};
return my;
}(MODULE));
此處我們重載<譯註:原文爲overridden,實爲改寫)了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));