The Module Pattern
Modules
模塊是任何健壯的應(yīng)用程序架構(gòu)的一個完整部分肤无,并且通常用來幫助保持一個項目代碼單元既整潔又有條理。
在JS中遍烦,有如下幾個關(guān)于實現(xiàn)模塊的選項愈污,他們包括
1.模塊模式
2.對象字面量表示法
3.AMD 模塊
4.CommonJs 模塊
5.ECMAScript Harmony 模塊
我們將在后面探索上述的后三種模式回季。
模塊模式部分是基于對象字面量的,所以首先認識這個知識對我們來說是有意義的秕重。
對象字面量
對象字面量是一個對象不同,通常用一組包含在{}大括號中的鍵值對描述。在對象字面量中的名稱通常是后跟冒號的字符串或者標識符。
在對象的最后一組鍵值對之后二拐,不應(yīng)該有逗號服鹅,否則可能會導致錯誤。
var myObjectLiteral = {
variableKey: variableValue,
functionKey: function () {
// ...
}
};
對象字面量不需要使用關(guān)鍵詞new 運算符創(chuàng)建百新,但是也不應(yīng)該把它用于一段代碼聲明的開始企软,就像打開的花括號{通常被解釋為一個塊的開始。在對象之外饭望,一個新的屬性可以通過賦值的方式被添加進對象仗哨,例如 myModule.property = "SOME VALUE"
通過下面的例子我們可以看到一個通過復(fù)雜字面量定義的模塊的例子。
var myModule = {
myProperty: "someValue",
//對象字面量可以包含屬性和方法杰妓,例如我們可以定一個對象來表是這個模塊的配置
myConfig: {
useCaching: true,
language: "en"
},
// 一個基本的方法
saySomething: function () {
console.log( "Where in the world is Paul Irish today?" );
},
// 一個基于現(xiàn)有配置進行輸出的方法
reportMyConfig: function () {
console.log( "Caching is: " + ( this.myConfig.useCaching ? "enabled" : "disabled") );
},
// 重寫現(xiàn)有配置的方法
updateMyConfig: function( newConfig ) {
if ( typeof newConfig === "object" ) {
this.myConfig = newConfig;
console.log( this.myConfig.language );
}
}
};
// =>: Where in the world is Paul Irish today?
myModule.saySomething();
// =>: Caching is: enabled
myModule.reportMyConfig();
// =>: fr
myModule.updateMyConfig({
language: "fr",
useCaching: false
});
// =>: Caching is: disabled
myModule.reportMyConfig();
利用對象字面量可以幫助你封裝和組織你的代碼藻治。如果我們選擇這種技術(shù),我們可能同樣會對模塊模式感興趣巷挥,它始終利用對象字面量桩卵,但僅僅是作為一個函數(shù)作用域的返回值。
The Module Pattern
模塊模式最初被定義為在傳統(tǒng)的軟件工程中為類定義提供公有和私有封裝的一種方式倍宾。
在JS中雏节,模塊模式被用來進一步模擬類的概念,通過在一個單例對象中包含公有的/私有的方法和變量高职,從全局作用域中屏蔽具體的細節(jié)部分钩乍。
這樣做的結(jié)果是我們減少了我們的函數(shù)名同該頁面上被定義的其他附加的函數(shù)沖突的可能性。
私有
模塊模式利用閉包來封裝私有的怔锌、狀態(tài)或者組織寥粹。它提供一種封裝公有和私有方法的混合方法,保護內(nèi)部的內(nèi)容不泄露到全局的作用域中并與其他的開發(fā)人員的接口相沖突埃元。在這種模式中涝涤,只有公有的方法會被返回,將所有其他的內(nèi)容都保留在了閉包內(nèi)岛杀。
這給我們提供了一個簡潔的解決方案阔拳,屏蔽內(nèi)部復(fù)雜邏輯并僅僅暴露我們的應(yīng)用其他部分希望使用的接口,該模式和立即被調(diào)用的函數(shù)表達式非常相似类嗤,除了返回一個對象而不是一個函數(shù)糊肠。
需要注意的是,js內(nèi)部確實沒有真正意義上的私有遗锣,因為它不像一些傳統(tǒng)的語言货裹,它沒有訪問修飾符。變量技術(shù)上不可以直接被聲明為共有的或者私有的黄伊,所以我們使用函數(shù)作用域來模擬這個概念泪酱,在模塊模式內(nèi)部,被聲明的變量或者方法,只能在模塊內(nèi)部使用墓阀,這歸功于閉包毡惜。然而在返回的對象中定義被定義的變量或者方法可以被任何人使用。
History
從歷史的角度看斯撮,模塊模式起初是被一群包括Richard Cornford的人在2003年開發(fā)出來的经伙。后來被道格拉斯克羅克福德(Douglas Crockford)在他的演講中推廣。另一件小事是勿锅,如果你曾經(jīng)使用過Yahoo的YUI庫帕膜,其中的一些功能呈現(xiàn)出這種十分熟悉的原因是模塊模式對YUI創(chuàng)建他們的組件時有強烈的影響。
例子
讓我們通過創(chuàng)建一個獨立的模塊來看看如何實現(xiàn)模塊模式
var testModule = (function () {
var counter = 0;
return {
incrementCounter: function () {
return counter++;
},
resetCounter: function () {
console.log( "counter value prior to reset: " + counter );
counter = 0;
}
};
})();
// 用法
// 增加數(shù)量
testModule.incrementCounter();
// 測試結(jié)果
// =>: counter value prior to reset: 1
testModule.resetCounter();
這里溢十,代碼的其他部分不可以直接訪問incrementCounter()
和resetCounter()
垮刹。這里的counter變量實際上是被我們從全局作用域屏蔽掉了,所以它看上去扮演的是私有變量——它的存在被限制在了模塊的閉包之中张弛,也因此我們能夠在模塊外直接訪問的代碼只有兩個函數(shù)荒典。我們的命名空間是有效的,因此在我們測試我們代碼的環(huán)節(jié)吞鸭,我們需要在任何調(diào)用的時候寺董,加入模塊名作為前綴。
當使用模塊模式時刻剥,我們可能會發(fā)現(xiàn)我們定義一個簡單的模板遮咖,對開始使用它是有用的。這有一個包含命名空間造虏,公有/私有變量的例子:
var myNamespace = (function () {
var myPrivateVar, myPrivateMethod;
// 一個私有的計數(shù)器變量
myPrivateVar = 0;
// 一個私有的方法御吞,可以打印任何輸入的變量
myPrivateMethod = function( foo ) {
console.log( foo );
};
return {
// 一個公有的變量
myPublicVar: "foo",
// 一個利用私有變量的公有函數(shù)/方法
myPublicFunction: function( bar ) {
//增加我們的私有變量
myPrivateVar++;
//調(diào)用我們私有的方法
myPrivateMethod( bar );
}
};
})();
來看另一個例子,如下我們可以看到一個使用這種模式實現(xiàn)的購物籃漓藕。這個模塊是完全獨立的在魄藕,在全局變量中被叫做basketModule
。其中basket
數(shù)組在模塊中保持私有狀態(tài)撵术,所以我們應(yīng)用中的其他部分是不可以直接讀取它的。它僅僅存在于模塊的閉包之中话瞧,并且在模塊外部能訪問的僅僅是它暴露出的方法嫩与,例如addItem()
,getItem()
。
var basketModule = (function () {
// privates
var basket = [];
function doSomethingPrivate() {
//...
}
function doSomethingElsePrivate() {
//...
}
// Return an object exposed to the public
return {
// Add items to our basket
addItem: function( values ) {
basket.push(values);
},
// Get the count of items in the basket
getItemCount: function () {
return basket.length;
},
// Public alias to a private function
doSomething: doSomethingPrivate,
// Get the total value of items in the basket
getTotal: function () {
var q = this.getItemCount(),
p = 0;
while (q--) {
p += basket[q].price;
}
return p;
}
};
})();
你可能會注意到交排,在模塊內(nèi)部划滋,我們返回了一個object,他們會被自動的賦值給basketModule,以便我們可以按照如下的方式與之互動埃篓。
// basketModule 模塊返回了一個我們可以使用的公用API對象处坪。
basketModule.addItem({
item: "bread",
price: 0.5
});
basketModule.addItem({
item: "butter",
price: 0.3
});
// => 2
console.log( basketModule.getItemCount() );
// => 0.8
console.log( basketModule.getTotal() );
// 然而下面的方式將不會工作
// => undefined
// 這是因為這個屬性沒有被這個模塊作為公有的API暴露出來
console.log( basketModule.basket );
// 不工作,理由同上,只存在于閉包內(nèi)同窘,沒有被作為公有的API暴露出來玄帕。
console.log( basket );
以上方法在命名空間(basketModule)內(nèi)部都是有效的。
注意如何界定上面的basket模塊中包含的功能想邦,即我們之后立即調(diào)用并存儲的返回值裤纹。這里有一系列的優(yōu)點如下:
1.擁有僅供我們模塊使用的公有和私有成員的自由,因為它們沒有暴露在頁面的其他部分(除了我們暴露的公有API)丧没,它們可以被看作是私有的鹰椒。
2.函數(shù)被正常的聲明和命名,當我們試圖去發(fā)現(xiàn)哪個函數(shù)拋出異常時將容易在調(diào)試工具中看到函數(shù)調(diào)用棧呕童。
3.T. J Crowder指出漆际,在過去,它也使我們能夠在不同的環(huán)境中返回不同的功能夺饲。在過去奸汇,我已經(jīng)看到開發(fā)人員使用這個進行UA測試,為了針對IE瀏覽器在他們的模塊中提供了一個代碼路徑钞支,但在當下我們可以選擇特性針對完成相似的目標茫蛹。
Module Pattern Variations
Import mixins(導入混入?)
這種模式的變化烁挟,展示了如何將全局變量作為函數(shù)參數(shù)婴洼,傳遞給一個定義我們模塊的匿名函數(shù)。這允許我們有效的將它導入到我們代碼的作用域并且命名為我們希望的別名撼嗓。
// 此處可以起別名
var myModule = (function ( jQ, _ ) {
function privateMethod1(){
jQ(".container").html("test");
}
function privateMethod2(){
console.log( _.min([10, 5, 100, 2, 1000]) );
}
return{
publicMethod: function(){
privateMethod1();
}
};
// 將jQuery 和 Underscore導入到匿名函數(shù)中
})( jQuery, _ );
myModule.publicMethod();
Exports(導出)
接下來的這個變化允許我們聲明全局變量而不去使用它柬采,有點像支持導入全局變量的概念,在接下來的例子中我們可以看到且警。
// Global module
var myModule = (function () {
//模塊對象
var module = {},
privateVariable = "Hello World";
function privateMethod() {
// ...
}
module.publicProperty = "Foobar";
module.publicMethod = function () {
console.log( privateVariable );
};
return module;
})();
工具包和特定框架的模塊模式實現(xiàn)
Dojo
Dojo提供了一些便利的方法處理對象粉捻,通過對象的方法調(diào)用dojo.setObject()
。用點分割的字符串斑芜,作為它的第一個參數(shù)肩刃,就像myObject.parent.child
中一個“child"作為"parent"的一個屬性,而"parent"則定義在"myObj"中杏头。使用 setObject()
允許我們?nèi)ピO(shè)置它的子對象盈包,如果在給定的路徑中對象不存在的話,創(chuàng)建中間的對象醇王。
例如你想創(chuàng)建一個baseket.core
作為store
命名空間下的對象呢燥,這可以用下面的一般性方式實現(xiàn)。
var store = window.store || {};
if ( !store["basket"] ) {
store.basket = {};
}
if ( !store.basket["core"] ) {
store.basket.core = {};
}
store.basket.core = {
// ...rest of our logic
};
或者使用 Dojo 1.7(AMD兼容版)這個庫并配合如下代碼實現(xiàn)
require(["dojo/_base/customStore"], function( store ){
// 使用 dojo.setObject()
store.setObject( "basket.core", (function() {
var basket = [];
function privateMethod() {
console.log(basket);
}
return {
publicMethod: function(){
privateMethod();
}
};
})());
});
如果想了解更多的關(guān)于dojo.setObject
的信息寓娩,請查看官方文檔
ExtJS
關(guān)于那些使用了Sencha的ExtJs的叛氨,下面的一個例子告訴你呼渣,如何利用框架正確的使用模塊模式。
這里寞埠,我們看到了一個關(guān)于如何定義命名空間屁置,然后利用包含公有和私有的API模塊填充它的例子。除了一些語法不同外畸裳,它與VanillaJs中的模塊模式的實現(xiàn)非常接近缰犁。
// 創(chuàng)建命名空間
Ext.namespace("myNameSpace");
// 在命名空間中創(chuàng)建應(yīng)用
myNameSpace.app = function () {
// 不要在這里創(chuàng)建引用DOM或者DOM元素
// 私有變量
var btn1,
privVar1 = 11;
// 私有方法
var btn1Handler = function ( button, event ) {
console.log( "privVar1=" + privVar1 );
console.log( "this.btn1Text=" + this.btn1Text );
};
// 公有方法
return {
// 公有屬性
btn1Text: "Button 1",
// 公有方法
init: function () {
if ( Ext.Ext2 ) {
btn1 = new Ext.Button({
renderTo: "btn1-ct",
text: this.btn1Text,
handler: btn1Handler
});
} else {
btn1 = new Ext.Button( "btn1-ct", {
text: this.btn1Text,
handler: btn1Handler
});
}
}
};
}();
YUI
相似的,當我們構(gòu)建我們的應(yīng)用程序的時候怖糊,也可以使用YUI3實現(xiàn)模塊模式帅容。
接下來的這個例子嚴重的依賴原來的YUI模塊模式,由Eric Miraglia實現(xiàn)伍伤,但是并徘,它與vanillaJs的版本存在著極大的不同。
Y.namespace( "store.basket" ) ;
Y.store.basket = (function () {
var myPrivateVar, myPrivateMethod;
// 私有變量:
myPrivateVar = "I can be accessed only within Y.store.basket.";
// 私有方法:
myPrivateMethod = function () {
Y.log( "I can be accessed only from within YAHOO.store.basket" );
}
return {
myPublicProperty: "I'm a public property.",
myPublicMethod: function () {
Y.log( "I'm a public method." );
// 在basket里面我們可以使用私有變量和方法
Y.log( myPrivateVar );
Y.log( myPrivateMethod() );
// 通過this訪問該作用域(當前返回對象)里的方法
Y.log( this.myPublicProperty );
}
};
})();
jQuery
這里有一些方法扰魂,jQuery代碼不指定插件也可以包裹在模塊模式中麦乞。Ben Cherry之前建議過一種實現(xiàn),將一些含有共同點的模塊劝评,通過包裹在函數(shù)中定義為一個模塊姐直。
在下面的例子中,一個library
方法被定義蒋畜,它聲明了一個庫声畏,并在它被創(chuàng)建的時候,自動的將它的inti方法綁定到了document.ready
方法中姻成。
function library( module ) {
$( function() {
if ( module.init ) {
module.init();
}
});
return module;
}
var myLibrary = library(function () {
return {
init: function () {
// module implementation
}
};
}());
優(yōu)點
我們已經(jīng)看到了模塊模式為何是有用的插龄,但是為什么模塊模式是一個好的選擇?對于一個初學者來說科展,有著面向?qū)ο蟊尘暗囊粋€開發(fā)者來說均牢,相比真正的封裝而言,這樣會更整潔才睹,至少從JS角度來看是這樣徘跪。
其實,它支持私有數(shù)據(jù)琅攘,所以在模塊模式中真椿,我們的公有部分的代碼可以接觸到私有的部分,然而模塊外的世界則不可接觸到模塊中的私有部分乎澄。
缺點
我們訪問公有的成員和私有的成員方式不同,所以當我們想要改變成員的可訪問性時测摔,我們實際上需要修改每個用到模塊成員的地方置济。
我們也無法在方法中訪問之后被添加到對象的私有成員解恰。也就是說,在許多情況下模塊模式仍是非常有用的浙于,當使用正確护盈,當然有潛力干山我們的應(yīng)用程序結(jié)構(gòu)。
其他的缺點包括無法為私有成員創(chuàng)建單元測試羞酗,當錯誤需要熱修復(fù)時腐宋,增加了額外的復(fù)雜度。修復(fù)私有的地方簡直是不可能的檀轨。相反胸竞,一個人必須修改所有的與有bug的私有成員交互的公有成員,開發(fā)者也無法輕易的擴展私有成員参萄,所以值得記住的是私有成員并不像最初呈現(xiàn)的那樣靈活卫枝。
關(guān)于更多關(guān)于模塊模式的內(nèi)容,可以參考Ben Cherry的優(yōu)秀的深入的文章讹挎。