模塊是任何健壯的應(yīng)用程序體系結(jié)構(gòu)不可或缺的一部分,特點(diǎn)是有助于保持應(yīng)用項(xiàng)目的代碼單元既能清晰地分離又有組織。
在JavaScript中,實(shí)現(xiàn)模塊有幾個(gè)選項(xiàng),他們包括:
我們在書中后面的現(xiàn)代模塊化JavaScript設(shè)計(jì)模式章節(jié)中將探討這些選項(xiàng)中的最后三個(gè)。
模塊化模式是基于對象的文字部分,所以首先對于更新我們對它們的知識是很有意義的。
在對象字面值的標(biāo)記里,一個(gè)對象被描述為一組以逗號分隔的名稱/值對括在大括號({})的集合。對象內(nèi)部的名稱可以是字符串或是標(biāo)記符后跟著一個(gè)冒號":"。在對象里最后一個(gè)名稱/值對不應(yīng)該以","為結(jié)束符,因?yàn)檫@樣會(huì)導(dǎo)致錯(cuò)誤。
var myObjectLiteral = {
variableKey: variableValue,
functionKey: function () {
// ...
};
};
對象字面值不要求使用新的操作實(shí)例,但是不能夠在結(jié)構(gòu)體開始使用,因?yàn)榇蜷_"{"可能被解釋為一個(gè)塊的開始。在對象外新的成員會(huì)被加載,使用分配如下:smyModule.property = "someValue"; 下面我們可以看到一個(gè)更完整的使用對象字面值定義一個(gè)模塊的例子:
var myModule = {
myProperty: "someValue",
// 對象字面值包含了屬性和方法(properties and methods).
// 例如,我們可以定義一個(gè)模塊配置進(jìn)對象:
myConfig: {
useCaching: true,
language: "en"
},
// 非?;镜姆椒? myMethod: function () {
console.log( "Where in the world is Paul Irish today?" );
},
// 輸出基于當(dāng)前配置(<span>configuration</span>)的一個(gè)值
myMethod2: function () {
console.log( "Caching is:" + ( this.myConfig.useCaching ) ? "enabled" : "disabled" );
},
// 重寫當(dāng)前的配置(configuration)
myMethod3: function( newConfig ) {
if ( typeof newConfig === "object" ) {
this.myConfig = newConfig;
console.log( this.myConfig.language );
}
}
};
// 輸出: Where in the world is Paul Irish today?
myModule.myMethod();
// 輸出: enabled
myModule.myMethod2();
// 輸出: fr
myModule.myMethod3({
language: "fr",
useCaching: false
});
使用對象字面值可以協(xié)助封裝和組織你的代碼。如果你想近一步了解對象字面值可以閱讀 Rebecca Murphey 寫過的關(guān)于此類話題的更深入的文章(depth)。
也就是說,如果我們選擇了這種技術(shù),我們可能對模塊模式有同樣的興趣。即使使用對象字面值,但也只有一個(gè)函數(shù)的返回值。
模塊化模式最初被定義為一種對傳統(tǒng)軟件工程中的類提供私有和公共封裝的方法。
在JavaScript中,模塊化模式用來進(jìn)一步模擬類的概念,通過這樣一種方式:我們可以在一個(gè)單一的對象中包含公共/私有的方法和變量,從而從全局范圍中屏蔽特定的部分。這個(gè)結(jié)果是可以減少我們的函數(shù)名稱與在頁面中其他腳本區(qū)域定義的函數(shù)名稱沖突的可能性。
模塊模式使用閉包的方式來將"私有信息",狀態(tài)和組織結(jié)構(gòu)封裝起來。提供了一種將公有和私有方法,變量封裝混合在一起的方式,這種方式防止內(nèi)部信息泄露到全局中,從而避免了和其它開發(fā)者接口發(fā)生沖圖的可能性。在這種模式下只有公有的API 會(huì)返回,其它將全部保留在閉包的私有空間中。
這種方法提供了一個(gè)比較清晰的解決方案,在只暴露一個(gè)接口供其它部分使用的情況下,將執(zhí)行繁重任務(wù)的邏輯保護(hù)起來。這個(gè)模式非常類似于立即調(diào)用函數(shù)式表達(dá)式(IIFE-查看命名空間相關(guān)章節(jié)獲取更多信息),但是這種模式返回的是對象,而立即調(diào)用函數(shù)表達(dá)式返回的是一個(gè)函數(shù)。
需要注意的是,在javascript事實(shí)上沒有一個(gè)顯式的真正意義上的"私有性"概念,因?yàn)榕c傳統(tǒng)語言不同,javascript沒有訪問修飾符。從技術(shù)上講,變量不能被聲明為公有的或者私有的,因此我們使用函數(shù)域的方式去模擬這個(gè)概念。在模塊模式中,因?yàn)殚]包的緣故,聲明的變量或者方法只在模塊內(nèi)部有效。在返回對象中定義的變量或者方法可以供任何人使用。
從歷史角度來看,模塊模式最初是在2003年由一群人共同發(fā)展出來的,這其中包括Richard Cornford。后來通過Douglas Crockford的演講,逐漸變得流行起來。另外一件事情是,如果你曾經(jīng)用過雅虎的YUI庫,你會(huì)看到其中的一些特性和模塊模式非常類似,而這種情況的原因是在創(chuàng)建YUI框架的時(shí)候,模塊模式極大的影響了YUI的設(shè)計(jì)。
下面這個(gè)例子通過創(chuàng)建一個(gè)自包含的模塊實(shí)現(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;
}
};
})();
// Usage:
// Increment our counter
testModule.incrementCounter();
// Check the counter value and reset
// Outputs: 1
testModule.resetCounter();
在這里我們看到,其它部分的代碼不能直接訪問我們的incrementCounter() 或者 resetCounter()的值。counter變量被完全從全局域中隔離起來了,因此其表現(xiàn)的就像一個(gè)私有變量一樣,它的存在只局限于模塊的閉包內(nèi)部,因此只有兩個(gè)函數(shù)可以訪問counter。我們的方法是有名字空間限制的,因此在我們代碼的測試部分,我們需要給所有函數(shù)調(diào)用前面加上模塊的名字(例如"testModule")。
當(dāng)使用模塊模式時(shí),我們會(huì)發(fā)現(xiàn)通過使用簡單的模板,對于開始使用模塊模式非常有用。下面是一個(gè)模板包含了命名空間,公共變量和私有變量。
var myNamespace = (function () {
var myPrivateVar, myPrivateMethod;
// A private counter variable
myPrivateVar = 0;
// A private function which logs any arguments
myPrivateMethod = function( foo ) {
console.log( foo );
};
return {
// A public variable
myPublicVar: "foo",
// A public function utilizing privates
myPublicFunction: function( bar ) {
// Increment our private counter
myPrivateVar++;
// Call our private method using bar
myPrivateMethod( bar );
}
};
})();
看一下另外一個(gè)例子,下面我們看到一個(gè)使用這種模式實(shí)現(xiàn)的購物車。這個(gè)模塊完全自包含在一個(gè)叫做basketModule 全局變量中。模塊中的購物車數(shù)組是私有的,應(yīng)用的其它部分不能直接讀取。只存在與模塊的閉包中,因此只有可以訪問其域的方法可以訪問這個(gè)變量。
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)部,你可能注意到我們返回了應(yīng)外一個(gè)對象。這個(gè)自動(dòng)賦值給了basketModule 因此我們可以這樣和這個(gè)對象交互。
// basketModule returns an object with a public API we can use
basketModule.addItem({
item: "bread",
price: 0.5
});
basketModule.addItem({
item: "butter",
price: 0.3
});
// Outputs: 2
console.log( basketModule.getItemCount() );
// Outputs: 0.8
console.log( basketModule.getTotal() );
// However, the following will not work:
// Outputs: undefined
// This is because the basket itself is not exposed as a part of our
// the public API
console.log( basketModule.basket );
// This also won't work as it only exists within the scope of our
// basketModule closure, but not the returned public object
console.log( basket );
上面的方法都處于basketModule 的名字空間中。
請注意在上面的basket模塊中 域函數(shù)是如何在我們所有的函數(shù)中被封裝起來的,以及我們?nèi)绾瘟⒓凑{(diào)用這個(gè)域函數(shù),并且將返回值保存下來。這種方式有以下的優(yōu)勢:
這個(gè)變體展示了如何將全局(例如 jQuery, Underscore)作為一個(gè)參數(shù)傳入模塊的匿名函數(shù)。這種方式允許我們導(dǎo)入全局,并且按照我們的想法在本地為這些全局起一個(gè)別名。
// Global module
var myModule = (function ( jQ, _ ) {
function privateMethod1(){
jQ(".container").html("test");
}
function privateMethod2(){
console.log( _.min([10, 5, 100, 2, 1000]) );
}
return{
publicMethod: function(){
privateMethod1();
}
};
// Pull in jQuery and Underscore
}( jQuery, _ ));
myModule.publicMethod();
這個(gè)變體允許我們聲明全局對象而不用使用它們,同樣也支持在下一個(gè)例子中我們將會(huì)看到的全局導(dǎo)入的概念。
// Global module
var myModule = (function () {
// Module object
var module = {},
privateVariable = "Hello World";
function privateMethod() {
// ...
}
module.publicProperty = "Foobar";
module.publicMethod = function () {
console.log( privateVariable );
};
return module;
}());
工具箱和框架特定的模塊模式實(shí)現(xiàn)。
Dojo提供了一個(gè)方便的方法 dojo.setObject() 來設(shè)置對象。這需要將以"."符號為第一個(gè)參數(shù)的分隔符,如:myObj.parent.child 是指定義在"myOjb"內(nèi)部的一個(gè)對象“parent”,它的一個(gè)屬性為"child"。使用setObject()方法允許我們設(shè)置children 的值,可以創(chuàng)建路徑傳遞過程中的任何對象即使這些它們根本不存在。
例如,如果我們聲明商店命名空間的對象basket.coreas,可以實(shí)現(xiàn)使用傳統(tǒng)的方式如下:
var store = window.store || {};
if ( !store["basket"] ) {
store.basket = {};
}
if ( !store.basket["core"] ) {
store.basket.core = {};
}
store.basket.core = {
// ...rest of our logic
};
或使用Dojo1.7(AMD兼容的版本)及以上如下:
require(["dojo/_base/customStore"], function( store ){
// using dojo.setObject()
store.setObject( "basket.core", (function() {
var basket = [];
function privateMethod() {
console.log(basket);
}
return {
publicMethod: function(){
privateMethod();
}
};
}()));
});
欲了解更多關(guān)于dojo.setObject()方法的信息,請參閱官方文檔 documentation
對于這些使用Sencha的ExtJS的人們,你們很幸運(yùn),因?yàn)楣俜轿臋n包含一些例子,用于展示如何正確地在框架里面使用模塊模式。
下面我們可以看到一個(gè)例子關(guān)于如何定義一個(gè)名字空間,然后填入一個(gè)包含有私有和公有API的模塊。除了一些語義上的不同之外,這個(gè)例子和使用vanilla javascript 實(shí)現(xiàn)的模塊模式非常相似。
// create namespace
Ext.namespace("myNameSpace");
// create application
myNameSpace.app = function () {
// do NOT access DOM from here; elements don't exist yet
// private variables
var btn1,
privVar1 = 11;
// private functions
var btn1Handler = function ( button, event ) {
console.log( "privVar1=" + privVar1 );
console.log( "this.btn1Text=" + this.btn1Text );
};
// public space
return {
// public properties, e.g. strings to translate
btn1Text: "Button 1",
// public methods
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
});
}
}
};
}();
類似地,我們也可以使用YUI3來實(shí)現(xiàn)模塊模式。下面的例子很大程度上是基于原始由Eric Miraglia實(shí)現(xiàn)的YUI本身的模塊模式,但是和vanillla Javascript 實(shí)現(xiàn)的版本比較起來差異不是很大。
Y.namespace( "store.basket" ) = (function () {
var myPrivateVar, myPrivateMethod;
// private variables:
myPrivateVar = "I can be accessed only within Y.store.basket.";
// private method:
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." );
// Within basket, I can access "private" vars and methods:
Y.log( myPrivateVar );
Y.log( myPrivateMethod() );
// The native scope of myPublicMethod is store so we can
// access public members using "this":
Y.log( this.myPublicProperty );
}
};
})();
因?yàn)閖Query編碼規(guī)范沒有規(guī)定插件如何實(shí)現(xiàn)模塊模式,因此有很多種方式可以實(shí)現(xiàn)模塊模式。Ben Cherry 之間提供一種方案,因?yàn)槟K之間可能存在大量的共性,因此通過使用函數(shù)包裝器封裝模塊的定義。
在下面的例子中,定義了一個(gè)library 函數(shù),這個(gè)函數(shù)聲明了一個(gè)新的庫,并且在新的庫(例如 模塊)創(chuàng)建的時(shí)候,自動(dòng)將初始化函數(shù)綁定到document的ready上。
function library( module ) {
$( function() {
if ( module.init ) {
module.init();
}
});
return module;
}
var myLibrary = library(function () {
return {
init: function () {
// module implementation
}
};
}());
既然我們已經(jīng)看到單例模式很有用,為什么還是使用模塊模式呢?首先,對于有面向?qū)ο蟊尘暗拈_發(fā)者來講,至少從javascript語言上來講,模塊模式相對于真正的封裝概念更清晰。
其次,模塊模式支持私有數(shù)據(jù)-因此,在模塊模式中,公共部分代碼可以訪問私有數(shù)據(jù),但是在模塊外部,不能訪問類的私有部分(沒開玩笑!感謝David Engfer 的玩笑)。
模塊模式的缺點(diǎn)是因?yàn)槲覀儾捎貌煌姆绞皆L問公有和私有成員,因此當(dāng)我們想要改變這些成員的可見性的時(shí)候,我們不得不在所有使用這些成員的地方修改代碼。
我們也不能在對象之后添加的方法里面訪問這些私有變量。也就是說,很多情況下,模塊模式很有用,并且當(dāng)使用正確的時(shí)候,潛在地可以改善我們代碼的結(jié)構(gòu)。
其它缺點(diǎn)包括不能為私有成員創(chuàng)建自動(dòng)化的單元測試,以及在緊急修復(fù)bug時(shí)所帶來的額外的復(fù)雜性。根本沒有可能可以對私有成員打補(bǔ)丁。相反地,我們必須覆蓋所有的使用存在bug私有成員的公共方法。開發(fā)者不能簡單的擴(kuò)展私有成員,因此我們需要記得,私有成員并非它們表面上看上去那么具有擴(kuò)展性。
想要了解更深入的信息,可以閱讀 Ben Cherry 這篇精彩的文章。
更多建議: