基礎知識
首先我們開始簡單概述模型模式。三年前Eric Miraglia(YUI)的博文使模型模式衆所周知。如果你已經很熟悉模型模式,可以直接閱讀“進階模式”。
匿名閉包
這是一切成為可能的基礎,也是JavaScript最好的特性。我們将簡單的建立匿名函數,并立即執行。所有函數内部代碼都在閉包(closure)内。它提供了整個應用生命周期的私有和狀态。
(function () {
// ... all vars and functions are in this scope only
// still maintains access to all globals
}());
注意匿名函數周圍的()。這是語言的要求。關鍵字function一般認為是函數聲明,包括()就是函數表達式。
引入全局
JavaScript有個特性,稱為隐性全局。使用變量名稱時,解釋器會從作用域向後尋找變量聲明。如果沒找到,變量會被假定入全局(以後可以全局調用)。如果會被配置設定使用,在還不存在時全局建立它。這意味着在匿名函數裡使用全局變量很簡單。不幸的是,這會導緻代碼難以管理,檔案中不容易區分(對人而言)哪個變量是全局的。
幸好,匿名函數還有一個不錯的選擇。全局變量作為參數傳遞給匿名函數。将它們引入我們的代碼中,既更清晰,又比使用隐性全局更快。下面是一個例子:
(function ($, YAHOO) {
// 目前域有權限通路全局jQuery($)和YAHOO
}(jQuery, YAHOO));
子產品出口
有時你不隻想用全局變量,但你需要先聲明他們(子產品的全局調用)。我們用匿名函數的傳回值,很容易輸出他們。這樣做就完成了基本的子產品模式。以下是一個完整例子:
var MODULE = (function () {
var my = {},
privateVariable = ;
function privateMethod() {
// ...
}
my.moduleProperty = ;
my.moduleMethod = function () {
// ...
};
return my;
}());
注意,我們聲明了一個全局子產品MODULE,有兩個公開屬性:方法MODULE.moduleMethod和屬性MODULE.moduleProperty。而且,匿名函數的閉包還維持了私有内部狀态。同時學會之上的内容,我們就很容易引入需要的全局變量,和輸出到全局變量。
進階模式
對許多使用者而言以上的還不足,我們可以采用以下的模式創造強大的,可擴充的結構。讓我們使用MODULE子產品,一個一個繼續。
擴充
子產品模式的一個限制是整個子產品必須在一個檔案裡。任何人都了解長代碼分割到不同檔案的必要。還好,我們有很好的辦法擴充子產品。(在擴充檔案)首先我們引入子產品(從全局),給他添加屬性,再輸出他。下面是一個例子擴充子產品:
var MODULE = (function (my) {
my.anotherMethod = function () {
// 此前的MODULE傳回my對象作為全局輸出,是以這個匿名函數的參數MODULE就是上面MODULE匿名函數裡的my
};
return my;
}(MODULE));
我們再次使用var關鍵字以保持一緻性,雖然其實沒必要。代碼執行後,子產品獲得一個新公開方法MODULE.anotherMethod。擴充檔案沒有影響子產品的私有内部狀态。
松耦合擴充
上面的例子需要我們首先建立子產品,然後擴充它,這并不總是必要的。提升JavaScript應用性能最好的操作就是異步加載腳本。因而我們可以建立靈活多部分的子產品,可以将他們無順序加載,以松耦合擴充。每個檔案應有如下的結構:
var MODULE = (function (my) {
// add capabilities...
return my;
}(MODULE || {}));
這個模式裡,var語句是必須的,以标記引入時不存在會建立。這意味着你可以像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;
},//子產品加載後,調用以移除對_private的通路權限
_unseal = my._unseal = my._unseal || function () {
my._private = _private;
my._seal = _seal;
my._unseal = _unseal;
};//子產品加載前,開啟對_private的通路,以實作擴充部分對私有内容的操作
// permanent access to _private, _seal, and _unseal
return my;
}(MODULE || {}));
任何檔案都可以在本地的變量_private中設定屬性,他會對别的擴充立即生效(即初始化時所有擴充的私有狀态都儲存在_private變量,并被my._private輸出)。子產品完全加載了,應用調用MODULE._seal()方法阻止對私有屬性的讀取(幹掉my._private輸出)。如果此後子產品又需要擴充,帶有一個私有方法。加載擴充檔案前調用MODULE._unseal()方法(恢複my._private,外部恢複操作權限)。加載後調用再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));