# 模塊化模式
## 模塊
模塊是任何健壯的應用程序體系結構不可或缺的一部分,特點是有助于保持應用項目的代碼單元既能清晰地分離又有組織。
在JavaScript中,實現模塊有幾個選項,他們包括:
* 模塊化模式
* 對象表示法
* AMD模塊
* CommonJS 模塊
* ECMAScript Harmony 模塊
我們在書中后面的現代模塊化JavaScript設計模式章節中將探討這些選項中的最后三個。
模塊化模式是基于對象的文字部分,所以首先對于更新我們對它們的知識是很有意義的。
### 對象字面值
在對象字面值的標記里,一個對象被描述為一組以逗號分隔的名稱/值對括在大括號({})的集合。對象內部的名稱可以是字符串或是標記符后跟著一個冒號":"。在對象里最后一個名稱/值對不應該以","為結束符,因為這樣會導致錯誤。
~~~
var myObjectLiteral = {
variableKey: variableValue,
functionKey: function () {
// ...
};
};
~~~
對象字面值不要求使用新的操作實例,但是不能夠在結構體開始使用,因為打開"{"可能被解釋為一個塊的開始。在對象外新的成員會被加載,使用分配如下:smyModule.property = "someValue"; 下面我們可以看到一個更完整的使用對象字面值定義一個模塊的例子:
~~~
var myModule = {
myProperty: "someValue",
// 對象字面值包含了屬性和方法(properties and methods).
// 例如,我們可以定義一個模塊配置進對象:
myConfig: {
useCaching: true,
language: "en"
},
// 非常基本的方法
myMethod: function () {
console.log( "Where in the world is Paul Irish today?" );
},
// 輸出基于當前配置(<span>configuration</span>)的一個值
myMethod2: function () {
console.log( "Caching is:" + ( this.myConfig.useCaching ) ? "enabled" : "disabled" );
},
// 重寫當前的配置(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
});
~~~
使用對象字面值可以協助封裝和組織你的代碼。如果你想近一步了解對象字面值可以閱讀 Rebecca Murphey 寫過的關于此類話題的更深入的文章[(depth)](http://rmurphey.com/blog/2009/10/15/using-objects-to-organize-your-code/)。
也就是說,如果我們選擇了這種技術,我們可能對模塊模式有同樣的興趣。即使使用對象字面值,但也只有一個函數的返回值。
## 模塊化模式
模塊化模式最初被定義為一種對傳統軟件工程中的類提供私有和公共封裝的方法。
在JavaScript中,模塊化模式用來進一步模擬類的概念,通過這樣一種方式:我們可以在一個單一的對象中包含公共/私有的方法和變量,從而從全局范圍中屏蔽特定的部分。這個結果是可以減少我們的函數名稱與在頁面中其他腳本區域定義的函數名稱沖突的可能性。
### 私有信息
模塊模式使用閉包的方式來將"私有信息",狀態和組織結構封裝起來。提供了一種將公有和私有方法,變量封裝混合在一起的方式,這種方式防止內部信息泄露到全局中,從而避免了和其它開發者接口發生沖圖的可能性。在這種模式下只有公有的API 會返回,其它將全部保留在閉包的私有空間中。
這種方法提供了一個比較清晰的解決方案,在只暴露一個接口供其它部分使用的情況下,將執行繁重任務的邏輯保護起來。這個模式非常類似于立即調用函數式表達式(IIFE-查看命名空間相關章節獲取更多信息),但是這種模式返回的是對象,而立即調用函數表達式返回的是一個函數。
需要注意的是,在javascript事實上沒有一個顯式的真正意義上的"私有性"概念,因為與傳統語言不同,javascript沒有訪問修飾符。從技術上講,變量不能被聲明為公有的或者私有的,因此我們使用函數域的方式去模擬這個概念。在模塊模式中,因為閉包的緣故,聲明的變量或者方法只在模塊內部有效。在返回對象中定義的變量或者方法可以供任何人使用。
#### 歷史
從歷史角度來看,模塊模式最初是在2003年由一群人共同發展出來的,這其中包括Richard Cornford。后來通過Douglas Crockford的演講,逐漸變得流行起來。另外一件事情是,如果你曾經用過雅虎的YUI庫,你會看到其中的一些特性和模塊模式非常類似,而這種情況的原因是在創建YUI框架的時候,模塊模式極大的影響了YUI的設計。
#### 例子
下面這個例子通過創建一個自包含的模塊實現了模塊模式。
~~~
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變量被完全從全局域中隔離起來了,因此其表現的就像一個私有變量一樣,它的存在只局限于模塊的閉包內部,因此只有兩個函數可以訪問counter。我們的方法是有名字空間限制的,因此在我們代碼的測試部分,我們需要給所有函數調用前面加上模塊的名字(例如"testModule")。
當使用模塊模式時,我們會發現通過使用簡單的模板,對于開始使用模塊模式非常有用。下面是一個模板包含了命名空間,公共變量和私有變量。
~~~
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 );
}
};
})();
~~~
看一下另外一個例子,下面我們看到一個使用這種模式實現的購物車。這個模塊完全自包含在一個叫做basketModule 全局變量中。模塊中的購物車數組是私有的,應用的其它部分不能直接讀取。只存在與模塊的閉包中,因此只有可以訪問其域的方法可以訪問這個變量。
~~~
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;
}
};
}());
~~~
在模塊內部,你可能注意到我們返回了應外一個對象。這個自動賦值給了basketModule 因此我們可以這樣和這個對象交互。
~~~
// 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模塊中 域函數是如何在我們所有的函數中被封裝起來的,以及我們如何立即調用這個域函數,并且將返回值保存下來。這種方式有以下的優勢:
* 可以創建只能被我們模塊訪問的私有函數。這些函數沒有暴露出來(只有一些API是暴露出來的),它們被認為是完全私有的。
* 當我們在一個調試器中,需要發現哪個函數拋出異常的時候,可以很容易的看到調用棧,因為這些函數是正常聲明的并且是命名的函數。
* 正如過去 T.J Crowder 指出的,這種模式同樣可以讓我們在不同的情況下返回不同的函數。我見過有開發者使用這種技巧用于執行UA(尿檢,抽樣檢查)測試,目的是為了在他們的模塊里面針對IE專門提供一條代碼路徑,但是現在我們也可以簡單的使用特征檢測達到相同的目的。
## 模塊模式的變體
### Import mixins(導入混合)
這個變體展示了如何將全局(例如 jQuery, Underscore)作為一個參數傳入模塊的匿名函數。這種方式允許我們導入全局,并且按照我們的想法在本地為這些全局起一個別名。
~~~
// 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();
~~~
### Exports(導出)
這個變體允許我們聲明全局對象而不用使用它們,同樣也支持在下一個例子中我們將會看到的全局導入的概念。
~~~
// 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;
}());
~~~
工具箱和框架特定的模塊模式實現。
### Dojo
Dojo提供了一個方便的方法 dojo.setObject() 來設置對象。這需要將以"."符號為第一個參數的分隔符,如:myObj.parent.child 是指定義在"myOjb"內部的一個對象“parent”,它的一個屬性為"child"。使用setObject()方法允許我們設置children 的值,可以創建路徑傳遞過程中的任何對象即使這些它們根本不存在。
例如,如果我們聲明商店命名空間的對象basket.coreas,可以實現使用傳統的方式如下:
~~~
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();
}
};
}()));
});
~~~
欲了解更多關于dojo.setObject()方法的信息,請參閱官方文檔?[documentation](http://dojotoolkit.org/reference-guide/1.7/dojo/setObject.html)
### ExtJS
對于這些使用Sencha的ExtJS的人們,你們很幸運,因為官方文檔包含一些例子,用于展示如何正確地在框架里面使用模塊模式。
下面我們可以看到一個例子關于如何定義一個名字空間,然后填入一個包含有私有和公有API的模塊。除了一些語義上的不同之外,這個例子和使用vanilla javascript 實現的模塊模式非常相似。
~~~
// 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
});
}
}
};
}();
~~~
### YUI
類似地,我們也可以使用YUI3來實現模塊模式。下面的例子很大程度上是基于原始由Eric Miraglia實現的YUI本身的模塊模式,但是和vanillla Javascript 實現的版本比較起來差異不是很大。
~~~
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 );
}
};
})();
~~~
### jQuery
因為jQuery編碼規范沒有規定插件如何實現模塊模式,因此有很多種方式可以實現模塊模式。Ben Cherry 之間提供一種方案,因為模塊之間可能存在大量的共性,因此通過使用函數包裝器封裝模塊的定義。
在下面的例子中,定義了一個library 函數,這個函數聲明了一個新的庫,并且在新的庫(例如 模塊)創建的時候,自動將初始化函數綁定到document的ready上。
~~~
function library( module ) {
$( function() {
if ( module.init ) {
module.init();
}
});
return module;
}
var myLibrary = library(function () {
return {
init: function () {
// module implementation
}
};
}());
~~~
### 優勢
既然我們已經看到單例模式很有用,為什么還是使用模塊模式呢?首先,對于有面向對象背景的開發者來講,至少從javascript語言上來講,模塊模式相對于真正的封裝概念更清晰。
其次,模塊模式支持私有數據-因此,在模塊模式中,公共部分代碼可以訪問私有數據,但是在模塊外部,不能訪問類的私有部分(沒開玩笑!感謝David Engfer 的玩笑)。
### 缺點
模塊模式的缺點是因為我們采用不同的方式訪問公有和私有成員,因此當我們想要改變這些成員的可見性的時候,我們不得不在所有使用這些成員的地方修改代碼。
我們也不能在對象之后添加的方法里面訪問這些私有變量。也就是說,很多情況下,模塊模式很有用,并且當使用正確的時候,潛在地可以改善我們代碼的結構。
其它缺點包括不能為私有成員創建自動化的單元測試,以及在緊急修復bug時所帶來的額外的復雜性。根本沒有可能可以對私有成員打補丁。相反地,我們必須覆蓋所有的使用存在bug私有成員的公共方法。開發者不能簡單的擴展私有成員,因此我們需要記得,私有成員并非它們表面上看上去那么具有擴展性。
想要了解更深入的信息,可以閱讀?[Ben Cherry](http://www.adequatelygood.com/JavaScript-Module-Pattern-In-Depth.html)?這篇精彩的文章。
- 前言
- 簡介
- 什么是設計模式?
- 設計模式的結構
- 編寫設計模式
- 反模式
- 設計模式的分類
- 設計模式分類概覽表
- JavaScript 設計模式
- 構造器模式
- 模塊化模式
- 暴露模塊模式
- 單例模式
- 觀察者模式
- 中介者模式
- 原型模式
- 命令模式
- 外觀模式
- 工廠模式
- Mixin 模式
- 裝飾模式
- 亨元(Flyweight)模式
- JavaScript MV* 模式
- MVC 模式
- MVP 模式
- MVVM 模式
- 最新的模塊化 JavaScript 設計模式
- AMD
- CommonJS
- ES Harmony
- JQuery 中的設計模式
- 組合模式
- 適配器模式
- 外觀模式
- 觀察者模式
- 迭代器模式
- 惰性初始模式
- 代理模式
- 建造者模式
- jQuery 插件的設計模式
- JavaScript 命名空間模式
- 總結
- 參考