<a name="a1"></a>
# 設計模式
在GoF(Gang of Four)的書中提出的設計模式為面向對象的軟件設計中遇到的一些普遍問題提供了解決方案。它們已經誕生很久了,而且被證實在很多情況下是很有效的。這正是你需要熟悉它的原因,也是我們要討論它的原因。
盡管這些設計模式跟語言和具體的實現方式無關,但它們多年來被關注到的方面仍然主要是在強類型靜態語言比如C++和Java中的應用。
JavaScript作為一種基于原型的弱類型動態語言,使得有些時候實現某些模式時相當簡單,甚至不費吹灰之力。
讓我們從第一個例子——單例模式——來看一下在JavaScript中和靜態的基于類的語言有什么不同。
<a name="a2"></a>
## 單例
單例模式的核心思想是讓指定的類只存在唯一一個實例。這意味著當你第二次使用相同的類去創建對象的時候,你得到的應該和第一次創建的是同一個對象。
這如何應用到JavaScript中呢?在JavaScript中沒有類,只有對象。當你創建一個對象時,事實上根本沒有另一個對象和它一樣,這個對象其實已經是一個單例。使用對象字面量創建一個簡單的對象也是一種單例的例子:
var obj = {
myprop: 'my value'
};
在JavaScript中,對象永遠不會相等,除非它們是同一個對象,所以即使你創建一個看起來完全一樣的對象,它也不會和前面的對象相等:
var obj2 = {
myprop: 'my value'
};
obj === obj2; // false
obj == obj2; // false
所以你可以說當你每次使用對象字面量創建一個對象的時候就是在創建一個單例,并沒有特別的語法遷涉進來。
> 需要注意的是,有的時候當人們在JavaScript中提出“單例”的時候,它們可能是在指第5章討論過的“模塊模式”。
<a name="a3"></a>
### 使用new
JavaScript沒有類,所以一字一句地說單例的定義并沒有什么意義。但是JavaScript有使用new、通過構造函數來創建對象的語法,有時候你可能需要這種語法下的一個單例實現。這也就是說當你使用new、通過同一個構造函數來創建多個對象的時候,你應該只是得到同一個對象的不同引用。
> 溫馨提示:從一個實用模式的角度來說,下面的討論并不是那么有用,只是更多地在實踐模擬一些語言中關于這個模式的一些問題的解決方案。這些語言主要是(靜態強類型的)基于類的語言,在這些語言中,函數并不是“一等公民”。
下面的代碼片段展示了期望的結果(假設你忽略了多元宇宙的設想,接受了只有一個宇宙的觀點):
var uni = new Universe();
var uni2 = new Universe();
uni === uni2; // true
在這個例子中,uni只在構造函數第一次被調用時創建。第二次(以及后續更多次)調用時,同一個uni對象被返回。這就是為什么uni === uni2的原因——因為它們實際上是同一個對象的兩個引用。那么怎么在JavaScript達到這個效果呢?
當對象實例this被創建時,你需要在Universe構造函數中緩存它,以便在第二次調用的時候返回。有幾種選擇可以達到這種效果:
- 你可以使用一個全局變量來存儲實例。不推薦使用這種方法,因為通常我們認為使用全局變量是不好的。而且,任何人都可以改寫全局變量的值,甚至可能是無意中改寫。所以我們不再討論這種方案。
- 你也可以將對象實例緩存在構造函數的屬性中。在JavaScript中,函數也是對象,所以它們也可以有屬性。你可以寫一些類似Universe.instance的屬性來緩存對象。這是一種漂亮干凈的解決方案,不足之處是instance屬性仍然是可以被公開訪問的,別人寫的代碼可能修改它,這樣就會失去這個實例。
- 你可以將實例包裹在閉包中。這可以保持實例是私有的,不會在構造函數之外被修改,代價是一個額外的閉包。
讓我們來看一下第二種和第三種方案的實現示例。
<a name="a4"></a>
### 將實例放到靜態屬性中
下面是一個將唯一的實例放入Universe構造函數的一個靜態屬性中的例子:
function Universe() {
// do we have an existing instance?
if (typeof Universe.instance === "object") {
return Universe.instance;
}
// proceed as normal
this.start_time = 0;
this.bang = "Big";
// cache
Universe.instance = this;
// implicit return:
// return this;
}
// testing
var uni = new Universe();
var uni2 = new Universe();
uni === uni2; // true
如你所見,這是一種直接有效的解決方案,唯一的缺陷是instance是可被公開訪問的。一般來說它被其它代碼誤刪改的可能是很小的(起碼比全局變量instance要小得多),但是仍然是有可能的。
<a name="a5"></a>
### 將實例放到閉包中
另一種實現基于類的單例模式的方法是使用一個閉包來保護這個唯一的實例。你可以通過第5章討論過的“私有靜態成員模式”來實現。唯一的秘密就是重寫構造函數:
function Universe() {
// the cached instance
var instance = this;
// proceed as normal
this.start_time = 0;
this.bang = "Big";
// rewrite the constructor
Universe = function () {
return instance;
};
}
// testing
var uni = new Universe();
var uni2 = new Universe();
uni === uni2; // true
第一次調用時,原始的構造函數被調用并且正常返回this。在后續的調用中,被重寫的構造函數被調用。被重寫怕這個構造函數可以通過閉包訪問私有的instance變量并且將它返回。
這個實現實際上也是第4章討論的自定義函數的又一個例子。如我們討論過的一樣,這種模式的缺點是被重寫的函數(在這個例子中就是構造函數Universe())將丟失那些在初始定義和重新定義之間添加的屬性。在這個例子中,任何添加到Universe()的原型上的屬性將不會被鏈接到使用原來的實現創建的實例上。(注:這里的“原來的實現”是指實例是由未被重寫的構造函數創建的,而Universe()則是被重寫的構造函數。)
下面我們通過一些測試來展示這個問題:
// adding to the prototype
Universe.prototype.nothing = true;
var uni = new Universe();
// again adding to the prototype
// after the initial object is created
Universe.prototype.everything = true;
var uni2 = new Universe();
Testing:
// only the original prototype was
// linked to the objects
uni.nothing; // true
uni2.nothing; // true
uni.everything; // undefined
uni2.everything; // undefined
// that sounds right:
uni.constructor.name; // "Universe"
// but that's odd:
uni.constructor === Universe; // false
uni.constructor不再和Universe()相同的原因是uni.constructor仍然是指向原來的構造函數,而不是被重新定義的那個。
如果一定被要求讓prototype和constructor的指向像我們期望的那樣,可以通過一些調整來做到:
function Universe() {
// the cached instance
var instance;
// rewrite the constructor
Universe = function Universe() {
return instance;
};
// carry over the prototype properties
Universe.prototype = this;
// the instance
instance = new Universe();
// reset the constructor pointer
instance.constructor = Universe;
// all the functionality
instance.start_time = 0;
instance.bang = "Big";
return instance;
}
現在所有的測試結果都可以像我們期望的那樣了:
// update prototype and create instance
Universe.prototype.nothing = true; // true
var uni = new Universe();
Universe.prototype.everything = true; // true
var uni2 = new Universe();
// it's the same single instance
uni === uni2; // true
// all prototype properties work
// no matter when they were defined
uni.nothing && uni.everything && uni2.nothing && uni2.everything; // true
// the normal properties work
uni.bang; // "Big"
// the constructor points correctly
uni.constructor === Universe; // true
另一種可選的解決方案是將構造函數和實例包在一個立即執行的函數中。當構造函數第一次被調用的時候,它返回一個對象并且將私有的instance指向它。在后續調用時,構造函數只是簡單地返回這個私有變量。在這種新的實現下,前面所有的測試代碼也會和期望的一樣:
var Universe;
(function () {
var instance;
Universe = function Universe() {
if (instance) {
return instance;
}
instance = this;
// all the functionality
this.start_time = 0;
this.bang = "Big";
};
}());
<a name="a6"></a>
## 工廠模式
使用工廠模式的目的就是創建對象。它通常被在類或者類的靜態方法中實現,目的是:
- 執行在建立相似的對象時進行的一些重復操作
- 讓工廠的使用者在編譯階段創建對象時不必知道它的特定類型(類)
第二點在靜態的基于類的語言中更重要,因為在(編譯階段)提前不知道類的情況下,創建類的實例是不普通的行為。但在JavaScript中,這部分的實現卻是相當容易的事情。
使用工廠方法(或類)創建的對象被設計為從同一個父對象繼承;它們是特定的實現一些特殊功能的子類。有些時候這個共同的父對象就是包含工廠方法的同一個類。
我們來看一個示例實現,我們有:
- 一個共同的父構造函數CarMaker。
- CarMaker的一個靜態方法叫factory(),用來創建car對象。
- 特定的從CarMaker繼承而來的構造函數CarMaker.Compact,CarMaker.SUV,CarMaker.Convertible。它們都被定義為父構造函數的靜態屬性以便保持全局空間干凈,同時在需要的時候我們也知道在哪里找到它們。
我們來看一下已經完成的實現會怎么被使用:
var corolla = CarMaker.factory('Compact');
var solstice = CarMaker.factory('Convertible');
var cherokee = CarMaker.factory('SUV');
corolla.drive(); // "Vroom, I have 4 doors"
solstice.drive(); // "Vroom, I have 2 doors"
cherokee.drive(); // "Vroom, I have 17 doors"
這一段:
var corolla = CarMaker.factory('Compact');
可能是工廠模式中最知名的。你有一個方法可以在運行時接受一個表示類型的字符串,然后它創建并返回了一個和請求的類型一樣的對象。這里沒有使用new的構造函數,也沒有看到任何對象字面量,僅僅只有一個函數根據一個字符串指定的類型創建了對象。
這里是一個工廠模式的示例實現,它能讓上面的代碼片段工作:
// parent constructor
function CarMaker() {}
// a method of the parent
CarMaker.prototype.drive = function () {
return "Vroom, I have " + this.doors + " doors";
};
// the static factory method
CarMaker.factory = function (type) {
var constr = type,
newcar;
// error if the constructor doesn't exist
if (typeof CarMaker[constr] !== "function") {
throw {
name: "Error",
message: constr + " doesn't exist"
};
}
// at this point the constructor is known to exist
// let's have it inherit the parent but only once
if (typeof CarMaker[constr].prototype.drive !== "function") {
CarMaker[constr].prototype = new CarMaker();
}
// create a new instance
newcar = new CarMaker[constr]();
// optionally call some methods and then return...
return newcar;
};
// define specific car makers
CarMaker.Compact = function () {
this.doors = 4;
};
CarMaker.Convertible = function () {
this.doors = 2;
};
CarMaker.SUV = function () {
this.doors = 24;
};
工廠模式的實現中沒有什么是特別困難的。你需要做的僅僅是尋找請求類型的對象的構造函數。在這個例子中,使用了一個簡單的名字轉換以便映射對象類型和創建對象的構造函數。繼承的部分只是一個公共的重復代碼片段的示例,它可以被放到工廠方法中而不是被每個構造函數的類型重復。(譯注:指通過原型繼承的代碼可以在factory方法以外執行,而不是放到factory中每調用一次都要執行一次。)
<a name="a7"></a>
### 內置對象工廠
作為一個“野生的工廠”的例子,我們來看一下內置的全局構造函數Object()。它的行為很像工廠,因為它根據不同的輸入創建不同的對象。如果傳入一個數字,它會使用Number()構造函數創建一個對象。在傳入字符串和布爾值的時候也會發生同樣的事情。任何其它的值(包括空值)將會創建一個正常的對象。
下面是這種行為的例子和測試,注意Object調用時可以不用加new:
var o = new Object(),
n = new Object(1),
s = Object('1'),
b = Object(true);
// test
o.constructor === Object; // true
n.constructor === Number; // true
s.constructor === String; // true
b.constructor === Boolean; // true
Object()也是一個工廠這一事實可能沒有太多實際用處,僅僅是覺得值得作為一個例子提一下,告訴我們工廠模式是隨處可見的。
<a name="a8"></a>
## 迭代器
在迭代器模式中,你有一些含有有序聚合數據的對象。這些數據可能在內部用一種復雜的結構存儲著,但是你希望提供一種簡單的方法來訪問這種結構中的每個元素。數據的使用者不需要知道你是怎樣組織你的數據的,他們只需要操作一個個獨立的元素。
在迭代器模式中,你的對象需要提供一個next()方法。按順序調用next()方法必須返回序列中的下一個元素,但是“下一個”在你的特定的數據結構中指什么是由你自己來決定的。
假設你的對象叫agg,你可以通過簡單地在循環中調用next()來訪問每個數據元素,像這樣:
var element;
while (element = agg.next()) {
// do something with the element ...
console.log(element);
}
在迭代器模式中,聚合對象通常也會提供一個方便的方法hasNext(),這樣對象的使用者就可以知道他們已經獲取到你數據的最后一個元素。當使用另一種方法——hasNext()——來按順序訪問所有元素時,是像這樣的:
while (agg.hasNext()) {
// do something with the next element...
console.log(agg.next());
}
<a name="a9"></a>
## 裝飾器
在裝飾器模式中,一些額外的功能可以在運行時被動態地添加到一個對象中。在靜態的基于類的語言中,處理這個問題可能是個挑戰,但是在JavaScript中,對象本來就是可變的,所以給一個對象添加額外的功能本身并不是什么問題。
裝飾器模式的一個很方便的特性是可以對我們需要的特性進行定制和配置。剛開始時,我們有一個擁有基本功能的對象,然后可以從可用的裝飾器中去挑選一些需要用到的去增加這個對象,甚至如果順序很重要的話,還可以指定增強的順序。
<a name="a10"></a>
### 用法
我們來看一下這個模式的示例用法。假設你正在做一個賣東西的web應用,每個新交易是一個新的sale對象。這個對象“知道”交易的價格并且可以通過調用sale.getPrice()方法返回。根據環境的不同,你可以開始用一些額外的功能來裝飾這個對象。假設一個場景是這筆交易是發生在加拿大的一個省Québec,在這種情況下,購買者需要付聯邦稅和Québec省稅。根據裝飾器模式的用法,你需要指明使用聯邦稅裝飾器和Québec省稅裝飾器來裝飾這個對象。然后你還可以給這個對象裝飾一些價格格式的功能。這個場景的使用方式可能是像這樣:
var sale = new Sale(100); // the price is 100 dollars
sale = sale.decorate('fedtax'); // add federal tax
sale = sale.decorate('quebec'); // add provincial tax
sale = sale.decorate('money'); // format like money
sale.getPrice(); // "$112.88"
在另一種場景下,購買者在一個不需要交省稅的省,并且你想用加拿大元的格式來顯示價格,你可以這樣做:
var sale = new Sale(100); // the price is 100 dollars
sale = sale.decorate('fedtax'); // add federal tax
sale = sale.decorate('cdn'); // format using CDN
sale.getPrice(); // "CDN$ 105.00"
如你所見,這是一種在運行時很靈活的方法來添加功能和調整對象。我們來看一下如何來實現這種模式。
<a name="a11"></a>
### 實現
一種實現裝飾器模式的方法是讓每個裝飾器成為一個擁有應該被重寫的方法的對象。每個裝飾器實際上是繼承自已經被前一個裝飾器增強過的對象。裝飾器的每個方法都會調用父對象(繼承自的對象)的同名方法并取得值,然后做一些額外的處理。
最終的效果就是當你在第一個例子中調用sale.getPrice()時,實際上是在調用money裝飾器的方法(圖7-1)。但是因為每個裝飾器會先調用父對象的方法,money的getPrice()先調用quebec的getPrice(),而它又會去調用fedtax的getPrice()方法,依次類推。這個鏈會一直走到原始的未經裝飾的由Sale()構造函數實現的getPrice()。

圖7-1 裝飾器模式的實現
這個實現以一個構造函數和一個原型方法開始:
function Sale(price) {
this.price = price || 100;
}
Sale.prototype.getPrice = function () {
return this.price;
};
裝飾器對象將都被作為構造函數的屬性實現:
Sale.decorators = {};
我們來看一個裝飾器的例子。這是一個對象,實現了一個自定義的getPrice()方法。注意這個方法首先從父對象的方法中取值然后修改這個值:
Sale.decorators.fedtax = {
getPrice: function () {
var price = this.uber.getPrice();
price += price * 5 / 100;
return price;
}
};
使用類似的方法我們可以實現任意多個需要的其它裝飾器。他們的實現方式像插件一樣來擴展核心的Sale()的功能。他們甚至可以被放到額外的文件中,被第三方的開發者來開發和共享:
Sale.decorators.quebec = {
getPrice: function () {
var price = this.uber.getPrice();
price += price * 7.5 / 100;
return price;
}
};
Sale.decorators.money = {
getPrice: function () {
return "$" + this.uber.getPrice().toFixed(2);
}
};
Sale.decorators.cdn = {
getPrice: function () {
return "CDN$ " + this.uber.getPrice().toFixed(2);
}
};
最后我們來看decorate()這個神奇的方法,它把所有上面說的片段都串起來了。記得它是這樣被調用的:
sale = sale.decorate('fedtax');
字符串'fedtax'對應在Sale.decorators.fedtax中實現的對象。被裝飾過的最新的對象newobj將從現在有的對象(也就是this對象,它要么是原始的對象,要么是經過最后一個裝飾器裝飾過的對象)中繼承。實現這一部分需要用到前面章節中提到的臨時構造函數模式。我們也設置一個uber屬性給newobj以便子對象可以訪問到父對象。然后我們從裝飾器中復制所有額外的屬性到被裝飾的對象newobj中。最后,在我們的例子中,newobj被返回并且成為被更新過的sale對象。
Sale.prototype.decorate = function (decorator) {
var F = function () {},
overrides = this.constructor.decorators[decorator],
i, newobj;
F.prototype = this;
newobj = new F();
newobj.uber = F.prototype;
for (i in overrides) {
if (overrides.hasOwnProperty(i)) {
newobj[i] = overrides[i];
}
}
return newobj;
};
<a name="a12"></a>
### 使用列表實現
我們來看另一個明顯不同的實現方法,受益于JavaScript的動態特性,它完全不需要使用繼承。同時,我們也可以簡單地將前一個方面的結果作為參數傳給下一個方法,而不需要每一個方法都去調用前一個方法。
這樣的實現方法還允許很容易地反裝飾(undecorating)或者撤銷一個裝飾,這僅僅需要從一個裝飾器列表中移除一個條目。
用法示例也會明顯簡單一些,因為我們不需要將decorate()的返回值賦值給對象。在這個實現中,decorate()不對對象做任何事情,它只是簡單地將裝飾器加入到一個列表中:
var sale = new Sale(100); // the price is 100 dollars
sale.decorate('fedtax'); // add federal tax
sale.decorate('quebec'); // add provincial tax
sale.decorate('money'); // format like money
sale.getPrice(); // "$112.88"
Sale()構造函數現在有了一個作為自己屬性的裝飾器列表:
function Sale(price) {
this.price = (price > 0) || 100;
this.decorators_list = [];
}
可用的裝飾器仍然被實現為Sale.decorators的屬性。注意getPrice()方法現在更簡單了,因為它們不需要調用父對象的getPrice()來獲取結果,結果已經作為參數傳遞給它們了:
Sale.decorators = {};
Sale.decorators.fedtax = {
getPrice: function (price) {
return price + price * 5 / 100;
}
};
Sale.decorators.quebec = {
getPrice: function (price) {
return price + price * 7.5 / 100;
}
};
Sale.decorators.money = {
getPrice: function (price) {
return "$" + price.toFixed(2);
}
};
最有趣的部分發生在父對象的decorate()和getPrice()方法上。在前一種實現方式中,decorate()還是多少有些復雜,而getPrice()十分簡單。在這種實現方式中事情反過來了:decorate()只需要往列表中添加條目而getPrice()做了所有的工作。這些工作包括遍歷現在添加的裝飾器的列表,然后調用它們的getPrice()方法,并將結果傳遞給前一個:
Sale.prototype.decorate = function (decorator) {
this.decorators_list.push(decorator);
};
Sale.prototype.getPrice = function () {
var price = this.price,
i,
max = this.decorators_list.length,
name;
for (i = 0; i < max; i += 1) {
name = this.decorators_list[i];
price = Sale.decorators[name].getPrice(price);
}
return price;
};
裝飾器模式的第二種實現方式更簡單一些,并且沒有引入繼承。裝飾的方法也會簡單。所有的工作都由“同意”被裝飾的方法來做。在這個示例實現中,getPrice()是唯一被允許裝飾的方法。如果你想有更多可以被裝飾的方法,那遍歷裝飾器列表的工作就需要由每個方法重復去做。但是,這可以很容易地被抽象到一個輔助方法中,給它傳一個方法然后使這個方法“可被裝飾”。如果這樣實現的話,decorators_list屬性就應該是一個對象,它的屬性名字是方法名,值是裝飾器對象的數組。
<a name="a13"></a>
## 策略模式
策略模式允許在運行的時候選擇算法。你的代碼的使用者可以在處理特定任務的時候根據即將要做的事情的上下文來從一些可用的算法中選擇一個。
使用策略模式的一個例子是解決表單驗證的問題。你可以創建一個validator對象,有一個validate()方法。這個方法被調用時不用區分具體的表單類型,它總是會返回同樣的結果——一個沒有通過驗證的列表和錯誤信息。
但是根據具體的需要驗證的表單和數據,你代碼的使用者可以選擇進行不同類別的檢查。你的validator選擇最佳的策略來處理這個任務,然后將具體的數據檢查工作交給合適的算法去做。
<a name="a14"></a>
### 數據驗證示例
假設你有一個下面這樣的數據,它可能來自頁面上的一個表單,你希望驗證它是不是有效的數據:
var data = {
first_name: "Super",
last_name: "Man",
age: "unknown",
username: "o_O"
};
對這個例子中的validator,它需要知道哪個是最佳策略,因此你需要先配置它,給它設定好規則以確定哪些是有效的數據。
假設你不需要姓,名字可以接受任何內容,但要求年齡是一個數字,并且用戶名只允許包含字母和數字。配置可能是這樣的:
validator.config = {
first_name: 'isNonEmpty',
age: 'isNumber',
username: 'isAlphaNum'
};
現在validator對象已經有了用來處理數據的配置,你可以調用validate()方法,然后將任何驗證錯誤打印到控制臺上:
validator.validate(data);
if (validator.hasErrors()) {
console.log(validator.messages.join("\n"));
}
它可能會打印出這樣的信息:
Invalid value for *age*, the value can only be a valid number, e.g. 1, 3.14 or 2010
Invalid value for *username*, the value can only contain characters and numbers, no special symbols
現在我們來看一下這個validator是如何實現的。所有可用的用來檢查的邏輯都是擁有一個validate()方法的對象,它們還有一行輔助信息用來顯示錯誤信息:
// checks for non-empty values
validator.types.isNonEmpty = {
validate: function (value) {
return value !== "";
},
instructions: "the value cannot be empty"
};
// checks if a value is a number
validator.types.isNumber = {
validate: function (value) {
return !isNaN(value);
},
instructions: "the value can only be a valid number, e.g. 1, 3.14 or 2010"
};
// checks if the value contains only letters and numbers
validator.types.isAlphaNum = {
validate: function (value) {
return !/[^a-z0-9]/i.test(value);
},
instructions: "the value can only contain characters and numbers, no special symbols"
};
最后,validator對象的核心是這樣的:
var validator = {
// all available checks
types: {},
// error messages in the current
// validation session
messages: [],
// current validation config
// name: validation type
config: {},
// the interface method
// `data` is key => value pairs
validate: function (data) {
var i, msg, type, checker, result_ok;
// reset all messages
this.messages = [];
for (i in data) {
if (data.hasOwnProperty(i)) {
type = this.config[i];
checker = this.types[type];
if (!type) {
continue; // no need to validate
}
if (!checker) { // uh-oh
throw {
name: "ValidationError",
message: "No handler to validate type " + type
};
}
result_ok = checker.validate(data[i]);
if (!result_ok) {
msg = "Invalid value for *" + i + "*, " + checker.instructions;
this.messages.push(msg);
}
}
}
return this.hasErrors();
},
// helper
hasErrors: function () {
return this.messages.length !== 0;
}
};
如你所見,validator對象是通用的,在所有的需要驗證的場景下都可以保持這個樣子。改進它的辦法就是增加更多類型的檢查。如果你將它用在很多頁面上,每快你就會有一個非常好的驗證類型的集合。然后在每個新的使用場景下你需要做的僅僅是配置validator然后調用validate()方法。
<a name="a15"></a>
## 外觀模式
外觀模式是一種很簡單的模式,它只是為對象提供了更多的可供選擇的接口。使方法保持短小而不是處理太多的工作是一種很好的實踐。在這種實踐的指導下,你會有一大堆的方法,而不是一個有著非常多參數的uber方法。有些時候,兩個或者更多的方法會經常被一起調用。在這種情況下,創建另一個將這些重復調用包裹起來的方法就變得意義了。
例如,在處理瀏覽器事件的時候,有以下的事件:
- stopPropagation()
阻止事件冒泡到父節點
- preventDefault()
阻止瀏覽器執行默認動作(如打開鏈接或者提交表單)
這是兩個有不同目的的相互獨立的方法,他們也應該被保持獨立,但與此同時,他們也經常被一起調用。所以為了不在應用中到處重復調用這兩個方法,你可以創建一個外觀方法來調用它們:
var myevent = {
// ...
stop: function (e) {
e.preventDefault();
e.stopPropagation();
}
// ...
};
外觀模式也適用于一些瀏覽器腳本的場景,即將瀏覽器的差異隱藏在一個外觀方法下面。繼續前面的例子,你可以添加一些處理IE中事件API的代碼:
var myevent = {
// ...
stop: function (e) {
// others
if (typeof e.preventDefault === "function") {
e.preventDefault();
}
if (typeof e.stopPropagation === "function") {
e.stopPropagation();
}
// IE
if (typeof e.returnValue === "boolean") {
e.returnValue = false;
}
if (typeof e.cancelBubble === "boolean") {
e.cancelBubble = true;
}
}
// ...
};
外觀模式在做一些重新設計和重構工作時也很有用。當你想用一個不同的實現來替換某個對象的時候,你可能需要工作相當長一段時間(一個復雜的對象),與此同時,一些使用這個新對象的代碼也在被同步編寫。你可以先想好新對象的API,然后使用新的API創建一個外觀方法在舊的對象前面。使用這種方式,當你完全替換到舊的對象的時候,你只需要修改少量客戶代碼,因為新的客戶代碼已經是在使用新的API了。
<a name="a16"></a>
## 代理模式
在代理設計模式中,一個對象充當了另一個對象的接口的角色。它和外觀模式不一樣,外觀模式帶來的方便僅限于將幾個方法調用聯合起來。而代理對象位于某個對象和它的客戶之間,可以保護對對象的訪問。
這個模式看起來開銷有點大,但在出于性能考慮時非常有用。代理對象可以作為對象(也叫“真正的主體”)的保護者,讓真正的主體對象做盡量少的工作。
一種示例用法是我們稱之為“懶初始化”(延遲初始化)的東西。假設初始化真正的主體是開銷很大的,并且正好客戶代碼將它初始化后并不真正使用它。在這種情況下,代理對象可以作為真正的主體的接口起到幫助作用。代理對象接收到初始化請求,但在真正的主體真正被使用之前都不會將它傳遞過去。
圖7-2展示了這個場景,當客戶代碼發出初始化請求時,代理對象回復一切就緒,但并沒有將請求傳遞過去,只有在客戶代碼真正需要真正的主體做些工作的時候才將兩個請求一起傳遞過去。

圖7-2 通過代理對象時客戶代碼與真正的主體的關系
<a name="a17"></a>
### 一個例子
在真正的主體做某件工作開銷很大時,代理模式很有用處。在web應用中,開銷最大的操作之一就是網絡請求,此時盡可能地合并HTTP請求是有意義的。我們來看一個這種場景下應用代理模式的實例。
#### 一個視頻列表(expando)
我們假設有一個用來播放選中視頻的應用。你可以在這里看到真實的例子<http://www.jspatterns.com/book/7/proxy.html>。
頁面上有一個視頻標題的列表,當用戶點擊視頻標題的時候,標題下方的區域會展開并顯示視頻的更多信息,同時也使得視頻可被播放。視頻的詳細信息和用來播放的URL并不是頁面的一部分,它們需要通過網絡請求來獲取。服務端可以接受多個視頻ID,這樣我們就可以在合適的時候通過一次請求多個視頻信息來減少HTTP請求以加快應用的速度。
我們的應用允許一次展開好幾個(或全部)視頻,所以這是一個合并網絡請求的絕好機會。

圖7-3 真實的視頻列表
#### 沒有代理對象的情況
這個應用中最主要的角色是兩個對象:
- videos
負責對信息區域展開/收起(videos.getInfo()方法)和播放視頻的響應(videos.getPlayer()方法)
- http
負責通過http.makeRequest()方法與服務端通訊
當沒有代理對象的時候,videos.getInfo()會為每個視頻調用一次http.makeRequest()方法。當我們添加代理對象proxy后,它將位于vidoes和http中間,接手對makeRequest()的調用,并在可能的時候合并請求。
我們首先看一下沒有代理對象的代碼,然后添加代理對象來提升應用的響應速度。
#### HTML
HTML代碼僅僅是一個鏈接列表:
<p><span id="toggle-all">Toggle Checked</span></p>
<ol id="vids">
<li><input type="checkbox" checked><a
href="http://new.music.yahoo.com/videos/--2158073">Gravedigger</a></li>
<li><input type="checkbox" checked><a
href="http://new.music.yahoo.com/videos/--4472739">Save Me</a></li>
<li><input type="checkbox" checked><a
href="http://new.music.yahoo.com/videos/--45286339">Crush</a></li>
<li><input type="checkbox" checked><a
href="http://new.music.yahoo.com/videos/--2144530">Don't Drink The Water</a></li>
<li><input type="checkbox" checked><a
href="http://new.music.yahoo.com/videos/--217241800">Funny the Way It Is</a></li>
<li><input type="checkbox" checked><a
href="http://new.music.yahoo.com/videos/--2144532">What Would You Say</a></li>
</ol>
#### 事件處理
現在我們來看一下事件處理的邏輯。首先我們定義一個方便的快捷函數$:
var $ = function (id) {
return document.getElementById(id);
};
使用事件代理(第8章有更多關于這個模式的內容),我們將所有id="vids"的條目上的點擊事件統一放到一個函數中處理:
$('vids').onclick = function (e) {
var src, id;
e = e || window.event;
src = e.target || e.srcElement;
if (src.nodeName !== "A") {
return;
}
if (typeof e.preventDefault === "function") {
e.preventDefault();
}
e.returnValue = false;
id = src.href.split('--')[1];
if (src.className === "play") {
src.parentNode.innerHTML = videos.getPlayer(id);
return;
}
src.parentNode.id = "v" + id;
videos.getInfo(id);
};
#### videos對象
videos對象有三個方法:
- getPlayer()
返回播放視頻需要的HTML代碼(跟我們討論的無關)
- updateList()
網絡請求的回調函數,接受從服務器返回的數據,然后生成用于視頻詳細信息的HTML代碼。這一部分也沒有什么太有趣的事情。
- getInfo()
這個方法切換視頻信息的可視狀態,同時也調用http對象的方法,并傳遞updaetList()作為回調函數。
下面是這個對象的代碼片段:
var videos = {
getPlayer: function (id) {...},
updateList: function (data) {...},
getInfo: function (id) {
var info = $('info' + id);
if (!info) {
http.makeRequest([id], "videos.updateList");
return;
}
if (info.style.display === "none") {
info.style.display = '';
} else {
info.style.display = 'none';
}
}
};
#### http對象
http對象只有一個方法,它向Yahoo!的YQL服務發起一個JSONP請求:
var http = {
makeRequest: function (ids, callback) {
var url = 'http://query.yahooapis.com/v1/public/yql?q=',
sql = 'select * from music.video.id where ids IN ("%ID%")',
format = "format=json",
handler = "callback=" + callback,
script = document.createElement('script');
sql = sql.replace('%ID%', ids.join('","'));
sql = encodeURIComponent(sql);
url += sql + '&' + format + '&' + handler;
script.src = url;
document.body.appendChild(script);
}
};
> YQL(Yahoo! Query Language)是一種web service,它提供了使用類似SQL的語法來調用很多其它web service的能力,使得使用者不需要學習每個service的API。
當所有的六個視頻都被選中后,將會向服務端發起六個獨立的像這樣的YQL請求:
select * from music.video.id where ids IN ("2158073")
#### 代理對象
前面的代碼工作得很正常,但我們可以讓它工作得更好。proxy對象就在這樣的場景中出現,并接管了http和videos對象之間的通訊。它將使用一個簡單的邏輯來嘗試合并請求:50ms的延遲。videos對象并不直接調用后臺接口,而是調用proxy對象的方法。proxy對象在轉發這個請求前將會等待一段時間,如果在等待的50ms內有另一個來自videos的調用,則它們將被合并為同一個請求。50ms的延遲對用戶來說幾乎是無感知的,但是卻可以用來合并請求以提升點擊“toggle”時的體驗,一次展開多個視頻。它也可以顯著降低服務器的負載,因為web服務器只需要處理更少量的請求。
合并后查詢兩個視頻信息的YQL大概是這樣:
select * from music.video.id where ids IN ("2158073", "123456")
在修改后的代碼中,唯一的變化是videos.getInfo()現在調用的是proxy.makeRequest()而不是http.makeRequest(),像這樣:
proxy.makeRequest(id, videos.updateList, videos);
proxy對象創建了一個隊列來收集50ms之內接受到的視頻ID,然后將這個隊列傳遞給http對象,并提供回調函數,因為videos.updateList()只能處理一次接收到的數據。(譯注:指每次傳入的回調函數只能處理當次接收到的數據。)
下面是proxy對象的代碼:
var proxy = {
ids: [],
delay: 50,
timeout: null,
callback: null,
context: null,
makeRequest: function (id, callback, context) {
// add to the queue
this.ids.push(id);
this.callback = callback;
this.context = context;
// set up timeout
if (!this.timeout) {
this.timeout = setTimeout(function () {
proxy.flush();
}, this.delay);
}
},
flush: function () {
http.makeRequest(this.ids, "proxy.handler");
// clear timeout and queue
this.timeout = null;
this.ids = [];
},
handler: function (data) {
var i, max;
// single video
if (parseInt(data.query.count, 10) === 1) {
proxy.callback.call(proxy.context, data.query.results.Video);
return;
}
// multiple videos
for (i = 0, max = data.query.results.Video.length; i < max; i += 1) {
proxy.callback.call(proxy.context, data.query.results.Video[i]);
}
}
};
了解代理模式后就在只簡單地改動一下原來的代碼的情況下,將多個web service請求合并為一個。
圖7-4和7-5展示了使用代理模式將與服務器三次數據交互(不用代理模式時)變為一次交互的過程。

圖7-4 與服務器三次數據交互

圖7-5 通過一個代理對象合并請求,減少與服務器數據交互
#### 使用代理對象做緩存
在這個例子中,客戶對象(videos)已經可以做到不對同一個對象重復發出請求。但現實情況中并不總是這樣。這個代理對象還可以通過緩存之前的請求結果到cache屬性中來進一步保護真正的主體http對象(圖7-6)。然后當videos對象需要對同一個ID的視頻請求第二次時,proxy對象可以直接從緩存中取出,從而避免一次網絡交互。

圖7-6 代理緩存
<a name="a18"></a>
## 中介者模式
一個應用不論大小,都是由一些彼此獨立的對象組成的。所有的對象都需要一個通訊的方式來保持可維護性,即你可以安全地修改應用的一部分而不破壞其它部分。隨著應用的開發和維護,會有越來越多的對象。然后,在重構代碼的時候,對象可能會被移除或者被重新安排。當對象知道其它對象的太多信息并且直接通訊(直接調用彼此的方法或者修改屬性)時,會導致我們不愿意看到的緊耦合。當對象耦合很緊時,要修改一個對象而不影響其它的對象是很困難的。此時甚至連一個最簡單的修改都變得不那么容易,甚至連一個修改需要用多長時間都難以評估。
中介者模式就是一個緩解此問題的辦法,它通過解耦來提升代碼的可維護性(見圖7-7)。在這個模式中,各個彼此合作的對象并不直接通訊,而是通過一個mediator(中介者)對象通訊。當一個對象改變了狀態后,它就通知中介者,然后中介者再將這個改變告知給其它應該知道這個變化的對象。

圖7-7 中介者模式中的對象關系
<a name="a19"></a>
### 中介者示例
我們來看一個使用中介者模式的實例。這個應用是一個游戲,它的玩法是比較兩位游戲者在半分鐘內按下按鍵的次數,次數多的獲勝。玩家1需要按的是1,玩家2需要按的是0(這樣他們的手指不會攪在一起)。當前分數會顯示在一個計分板上。
對象列表如下:
- Player 1
- Player 2
- Scoreboard
- Mediator
中介者Mediator知道所有的對象。它與輸入設備(鍵盤)打交道,處理keypress事件,決定現在是哪位玩家玩的,然后通知這個玩家(見圖7-8)。玩家負責玩(即給自己的分數加一分),然后通知中介者他這一輪已經玩完。中介者再告知計分板最新的分數,計分板更新顯示。
除了中介者之外,其它的對象都不知道有別的對象存在。這樣就使得更新這個游戲變得很簡單,比如要添加一位玩家或者是添加另外一個顯示剩余時間的地方。
你可以在這里看到這個游戲的在線演示<http://jspatterns.com/book/7/mediator.html>。

圖7-8 游戲涉及的對象
玩家對象是通過Player()構造函數來創建的,有自己的points和name屬性。原型上的play()方法負責給自己加一分然后通知中介者:
function Player(name) {
this.points = 0;
this.name = name;
}
Player.prototype.play = function () {
this.points += 1;
mediator.played();
};
scoreboard對象(計分板)有一個update()方法,它會在每次玩家玩完后被中介者調用。計分析根本不知道玩家的任何信息,也不保存分數,它只負責顯示中介者給過來的分數:
var scoreboard = {
// HTML element to be updated
element: document.getElementById('results'),
// update the score display
update: function (score) {
var i, msg = '';
for (i in score) {
if (score.hasOwnProperty(i)) {
msg += '<p><strong>' + i + '<\/strong>: ';
msg += score[i];
msg += '<\/p>';
}
}
this.element.innerHTML = msg;
}
};
現在我們來看一下mediator對象(中介者)。在游戲初始化的時候,在setup()方法中創建游戲者,然后放后players屬性以便后續使用。played()方法會被游戲者在每輪玩完后調用,它更新score哈希然表然后將它傳給scoreboard用于顯示。最后一個方法是keypress(),負責處理鍵盤事件,決定是哪位玩家玩的,并且通知它:
var mediator = {
// all the players
players: {},
// initialization
setup: function () {
var players = this.players;
players.home = new Player('Home');
players.guest = new Player('Guest');
},
// someone plays, update the score
played: function () {
var players = this.players,
score = {
Home: players.home.points,
Guest: players.guest.points
};
scoreboard.update(score);
},
// handle user interactions
keypress: function (e) {
e = e || window.event; // IE
if (e.which === 49) { // key "1"
mediator.players.home.play();
return;
}
if (e.which === 48) { // key "0"
mediator.players.guest.play();
return;
}
}
};
最后一件事是初始化和結束游戲:
// go!
mediator.setup();
window.onkeypress = mediator.keypress;
// game over in 30 seconds
setTimeout(function () {
window.onkeypress = null;
alert('Game over!');
}, 30000);
<a name="a20"></a>
## 觀察者模式
觀察者模式被廣泛地應用于JavaScript客戶端編程中。所有的瀏覽器事件(mouseover,keypress等)都是使用觀察者模式的例子。這種模式的另一個名字叫“自定義事件”,意思是這些事件是被編寫出來的,和瀏覽器觸發的事件相對。它還有另外一個名字叫“訂閱者/發布者”模式。
使用這個模式的最主要目的就是促進代碼觸解耦。在觀察者模式中,一個對象訂閱另一個對象的指定活動并得到通知,而不是調用另一個對象的方法。訂閱者也被叫作觀察者,被觀察的對象叫作發布者或者被觀察者(譯注:subject,不知道如何翻譯,第一次的時候譯為“主體”,第二次譯時覺得不妥,還是直接叫被觀察者好了)。當一個特定的事件發生的時候,發布者會通知(調用)所有的訂閱者,同時還可能以事件對象的形式傳遞一些消息。
<a name="a21"></a>
### 例1:雜志訂閱
為了理解觀察者模式的實現方式,我們來看一個具體的例子。我們假設有一個發布者paper,它發行一份日報和一份月刊。無論是日報還是月刊發行,有一個名叫joe的訂閱者都會收到通知。
paper對象有一個subscribers屬性,它是一個數組,用來保存所有的訂閱者。訂閱的過程就僅僅是將訂閱者放到這個數組中而已。當一個事件發生時,paper遍歷這個訂閱者列表,然后通知它們。通知的意思也就是調用訂閱者對象的一個方法。因此,在訂閱過程中,訂閱者需要提供一個方法給paper對象的subscribe()。
paper對象也可以提供unsubscribe()方法,它可以將訂閱者從數組中移除。paper對象的最后一個重要的方法是publish(),它負責調用訂閱者的方法。總結一下,一個發布者對象需要有這些成員:
- subscribers
一個數組
- subscribe()
將訂閱者加入數組
- unsubscribe()
從數組中移除訂閱者
- publish()
遍歷訂閱者并調用它們訂閱時提供的方法
所有三個方法都需要一個type參數,因為一個發布者可能觸發好幾種事件(比如同時發布雜志和報紙),而訂閱者可以選擇性地訂閱其中的一種或幾種。
因為這些成員對任何對象來說都是通用的,因此將它們作為獨立對象的一部分提取出來是有意義的。然后,我們可以(通過混元模式)將它們復制到任何一個對象中,將這些對象轉換為訂閱者。
下面是這些發布者通用功能的一個示例實現,它定義了上面列出來的所有成員,還有一個輔助的visitSubscribers()方法:
var publisher = {
subscribers: {
any: [] // event type: subscribers
},
subscribe: function (fn, type) {
type = type || 'any';
if (typeof this.subscribers[type] === "undefined") {
this.subscribers[type] = [];
}
this.subscribers[type].push(fn);
},
unsubscribe: function (fn, type) {
this.visitSubscribers('unsubscribe', fn, type);
},
publish: function (publication, type) {
this.visitSubscribers('publish', publication, type);
},
visitSubscribers: function (action, arg, type) {
var pubtype = type || 'any',
subscribers = this.subscribers[pubtype],
i,
max = subscribers.length;
for (i = 0; i < max; i += 1) {
if (action === 'publish') {
subscribers[i](arg);
} else {
if (subscribers[i] === arg) {
subscribers.splice(i, 1);
}
}
}
}
};
下面這個函數接受一個對象作為參數,并通過復制通用的發布者的方法將這個對象墨跡成發布者:
function makePublisher(o) {
var i;
for (i in publisher) {
if (publisher.hasOwnProperty(i) && typeof publisher[i] === "function") {
o[i] = publisher[i];
}
}
o.subscribers = {any: []};
}
現在我們來實現paper對象,它能做的事情就是發布日報和月刊:
var paper = {
daily: function () {
this.publish("big news today");
},
monthly: function () {
this.publish("interesting analysis", "monthly");
}
};
將paper對象變成發布者:
makePublisher(paper);
現在我們有了一個發布者,讓我們再來看一下訂閱者對象joe,它有兩個方法:
var joe = {
drinkCoffee: function (paper) {
console.log('Just read ' + paper);
},
sundayPreNap: function (monthly) {
console.log('About to fall asleep reading this ' + monthly);
}
};
現在讓joe來訂閱paper:
paper.subscribe(joe.drinkCoffee);
paper.subscribe(joe.sundayPreNap, 'monthly');
如你所見,joe提供了一個當默認的any事件發生時被調用的方法,還提供了另一個當monthly事件發生時被調用的方法。現在讓我們來觸發一些事件:
paper.daily();
paper.daily();
paper.daily();
paper.monthly();
這些發布行為都會調用joe的對應方法,控制臺中輸出的結果是:
Just read big news today
Just read big news today
Just read big news today
About to fall asleep reading this interesting analysis
這里值得稱道的地方就是paper對象并沒有硬編碼寫上joe,而joe也同樣沒有硬編碼寫上paper。這里也沒有知道所有事情的中介者對象。所有涉及到的對象都是松耦合的,而且在不修改代碼的前提下,我們可以給paper添加更多的訂閱者,同時joe也可以在任何時候取消訂閱。
讓我們更進一步,將joe也變成一個發布者。(畢竟,在博客和微博上,任何人都可以是發布者。)這樣,joe變成發布者之后就可以在Twitter上更新狀態:
makePublisher(joe);
joe.tweet = function (msg) {
this.publish(msg);
};
現在假設paper的公關部門準備通過Twitter收集讀者反饋,于是它訂閱了joe,提供了一個方法readTweets():
paper.readTweets = function (tweet) {
alert('Call big meeting! Someone ' + tweet);
};
joe.subscribe(paper.readTweets);
這樣每當joe發出消息時,paper就會彈出警告窗口:
joe.tweet("hated the paper today");
結果是一個警告窗口:“Call big meeting! Someone hated the paper today”。
你可以在<http://jspatterns.com/book/7/observer.html>看到完整的源代碼,并且在控制臺中運行這個實例。
<a name="a22"></a>
### 例2:按鍵游戲
我們來看另一個例子。我們將實現一個和中介者模式的示例一樣的按鈕游戲,但這次使用觀察者模式。為了讓它看起來更高檔,我們允許接受無限個玩家,而不限于2個。我們仍然保留用來產生玩家的Player()構造函數,也保留scoreboard對象。只有mediator會變成game對象。
在中介者模式中,mediator對象知道所有涉及到的對象,并且調用它們的方法。而觀察者模式中的game對象不是這樣,它會讓對象來訂閱它們感興趣的事件。比如,scoreboard會訂閱game對象的scorechange事件。
首先我們重新看一下通用的publisher對象,并且將它的接口做一點小修改以更貼近瀏覽器的情況:
- 將publish(),subscribe(),unsubscribe()分別改為fire(),on(),remove()
- 事件的type每次都會被用到,所以把它變成三個方法的第一個參數
- 可以給訂閱者的方法額外加一個context參數,以便回調方法可以用this指向它自己所屬的對象
新的publisher對象是這樣:
var publisher = {
subscribers: {
any: []
},
on: function (type, fn, context) {
type = type || 'any';
fn = typeof fn === "function" ? fn : context[fn];
if (typeof this.subscribers[type] === "undefined") {
this.subscribers[type] = [];
}
this.subscribers[type].push({fn: fn, context: context || this});
},
remove: function (type, fn, context) {
this.visitSubscribers('unsubscribe', type, fn, context);
},
fire: function (type, publication) {
this.visitSubscribers('publish', type, publication);
},
visitSubscribers: function (action, type, arg, context) {
var pubtype = type || 'any',
subscribers = this.subscribers[pubtype],
i,
max = subscribers ? subscribers.length : 0;
for (i = 0; i < max; i += 1) {
if (action === 'publish') {
subscribers[i].fn.call(subscribers[i].context, arg);
} else {
if (subscribers[i].fn === arg && subscribers[i].context === context) {
subscribers.splice(i, 1);
}
}
}
}
};
新的Player()構造函數是這樣:
function Player(name, key) {
this.points = 0;
this.name = name;
this.key = key;
this.fire('newplayer', this);
}
Player.prototype.play = function () {
this.points += 1;
this.fire('play', this);
};
變動的部分是這個構造函數接受key,代表這個玩家在鍵盤上用來按之后得分的按鍵。(這些鍵預先被硬編碼過。)每次創建一個新玩家的時候,一個newplayer事件也會被觸發。類似的,每次有一個玩家玩的時候,會觸發play事件。
scoreboard對象和原來一樣,它只是簡單地將當前分數顯示出來。
game對象會關注所有的玩家,這樣它就可以給出分數并且觸發scorechange事件。它也會訂閱瀏覽呂中所有的keypress事件,這樣它就會知道按鈕對應的玩家:
var game = {
keys: {},
addPlayer: function (player) {
var key = player.key.toString().charCodeAt(0);
this.keys[key] = player;
},
handleKeypress: function (e) {
e = e || window.event; // IE
if (game.keys[e.which]) {
game.keys[e.which].play();
}
},
handlePlay: function (player) {
var i,
players = this.keys,
score = {};
for (i in players) {
if (players.hasOwnProperty(i)) {
score[players[i].name] = players[i].points;
}
}
this.fire('scorechange', score);
}
};
用于將任意對象轉變為訂閱者的makePublisher()還是和之前一樣。game對象會變成發布者(這樣它才可以觸發scorechange事件),Player.prototype也會變成發布者,以使得每個玩家對象可以觸發play和newplayer事件:
makePublisher(Player.prototype);
makePublisher(game);
game對象訂閱play和newplayer事件(以及瀏覽器的keypress事件),scoreboard訂閱scorechange事件:
Player.prototype.on("newplayer", "addPlayer", game);
Player.prototype.on("play", "handlePlay", game);
game.on("scorechange", scoreboard.update, scoreboard);
window.onkeypress = game.handleKeypress;
如你所見,on()方法允許訂閱者通過函數(scoreboard.update)或者是字符串("addPlayer")來指定回調函數。當有提供context(如game)時,才能通過字符串來指定回調函數。
初始化的最后一點工作就是動態地創建玩家對象(以及它們對象的按鍵),用戶想要多少個就可以創建多少個:
var playername, key;
while (1) {
playername = prompt("Add player (name)");
if (!playername) {
break;
}
while (1) {
key = prompt("Key for " + playername + "?");
if (key) {
break;
}
}
new Player(playername, key);
}
這就是游戲的全部。你可以在<http://jspatterns .com/book/7/observer-game.html>看到完整的源代碼并且試玩一下。
值得注意的是,在中介者模式中,mediator對象必須知道所有的對象,然后在適當的時機去調用對應的方法。而這個例子中,game對象會顯得笨一些(譯注:指知道的信息少一些),游戲依賴于對象去觀察特寫的事件然后觸發相應的動作:如scoreboard觀察scorechange事件。這使得對象之間的耦合更松了(對象間知道彼此的信息越少越好),而代價則是弄清事件和訂閱者之間的對應關系會更困難一些。在這個例子中,所有的訂閱行為都發生在代碼中的同一個地方,而隨著應用規模的境長,on()可能會被在各個地方調用(如在每個對象的初始化代碼中)。這使得調試更困難一些,因為沒有一個集中的地方來看這些代碼并理解正在發生什么事情。在觀察者模式中,你將不再能看到那種從開頭一直跟到結尾的順序執行方式。
<a name="a23"></a>
## 小結
在這章中你學習到了若干種流行的設計模式,并且也知道了如何在JavaScript中實現它們。我們討論過的設計模式有:
- 單例模式
只創建類的唯一一個實例。我們看了好幾種可以不通過構造函數和類Java語法達成單例的方法。從另一方面來說,JavaScript中所有的對象都是單例。有時候開發者說的單例是指通過模塊化模式創建的對象。
- 工廠模式
一種在運行時通過指定字符串來創建指定類型對象的方法。
- 遍歷模式
通過提供API來實現復雜的自定義數據結構中的遍歷和導航。
- 裝飾模式
在運行時通過從預先定義好的裝飾器對象來給被裝飾對象動態添加功能。
- 策略模式
保持接口一致的情況下選擇最好的策略來完成特寫類型的任務。
- 外觀模式
通過包裝通用的(或者設計得很差的)方法來提供一個更方便的API。
- 代理模式
包裝一個對象以控制對它的訪問,通過合并操作或者是只在真正需要時執行來盡量避免開銷太大的操作。
- 中介者模式
通過讓對象不彼此溝通,只通過一個中介者對象溝通的方法來促進解耦。
- 觀察者模式
通過創建“可被觀察的對象”使它在某個事件發生時通知訂閱者的方式來解耦。(也叫“訂閱者/發布者”或者“自定義事件”。)