接觸前端兩三個月的時候,那時候只是聽說設計模式很重要,然后我就去讀了一本設計模式的書,讀了一部分,也不知道這些設計模式到底設計出來干嘛的,然后就沒再看了。后來就自己做一些小項目也覺著好像不需要用到設計模式這個東西呀。現在,接觸前端有半年了,決定再重新看看設計模式,說不定會有一些啟發。于是發現了一本好書——《JavaScript設計模式》,寫的通俗易懂,用一個個故事串起了一整本書,看了一部分發現原來我平時寫代碼的時候無意之中就用到了一些設計模式,然后就忍不住都看完了。看完整本書,讓我完全改變了以前對設計模式的看法,也學到了很多在實際項目開發中的經驗。這里就簡單總結下這本書,也算是做個筆記,供自己以后參考。(定義一般都比較晦澀難懂,可以先看看使用場景再回來理解相關定義)
先給個書的鏈接: JavaScript設計模式-張容銘
什么是設計模式
設計模式是代碼設計經驗的總結,為了可重用代碼,保證代碼的可靠性等。設計模式主要分為三大類型,創建型模式,結構型模式和行為型模式,本書還額外寫了另兩類設計模式,技巧型模式和架構型模式。JavaScript設計模式是以面向對象編程為基礎的,JavaScript的面向對象編程和傳統的C++、Java的面向對象編程有些差別,這讓我一開始接觸JavaScript的時候感到十分痛苦,但是這只能靠自己慢慢積累慢慢思考。想繼續了解JavaScript設計模式必須要先搞懂JavaScript面向對象編程,否則只會讓你自己更痛苦。
創建型設計模式
創建型設計模式是一類處理對象創建的設計模式,通過某種方式控制對象的創建來避免基本對象創建時可能導致設計上的問題或增加設計上的復雜度。創建型設計模式主要有簡單工廠模式,工廠方法模式,抽象工廠模式,建造者模式,原型模式和單例模式,下面一一道來。
簡單工廠模式
作者把簡單工廠模式比喻成一個神奇的魔術師。
定義
又叫靜態工廠方法,由一個工廠對象決定創建某一種產品對象類的實例,主要用來創建同一類對象。
使用場景
看完上面的定義一定很不解,說的到底是啥,現在就舉個例子來解釋一下。比如體育商品店賣體育器材,里面有很多體育用品及其相關介紹。當你來到體育用品店買一個籃球,只需問售貨員,他就會幫你找到你所要的東西。用程序實現如下:
// 籃球基類
var Basketball = function() {
this.intro = '籃球盛行于美國';
};
Basketball.prototype = {
getMember: function() {
console.log('每個隊伍需要5名隊員');
},
getBallSize: function() {
console.log('籃球很大');
}
};
// 足球基類
var Football = function() {
this.intro = '足球盛行于美國';
};
Football.prototype = {
getMember: function() {
console.log('每個隊伍需要11名隊員');
},
getBallSize: function() {
console.log('籃球很大');
}
};
// 運動工廠
var SportsFactory = function(name) {
switch(name) {
case 'NBA':
return new Basketball();
case 'wordCup':
return new Football();
}
};
當你使用這個運動工廠時只需要記住SportsFactory這個工廠對象就好了,它會幫你找到你想要的。
簡單工廠模式的理念是創建對象,上面例子是將不同的類實例化,但是簡單工廠模式還可以創建相似對象,將相似的東西提取,不相似的針對性處理即可。這樣只需創建一個對象就可以替代多個類了。
收獲與總結
團隊開發不同于個人,對全局變量的限制很大,要盡量少得創建全局變量。如果有同一類對象在不同需求中重復使用,那么大部分是不需要重復創建的,要學會代碼復用。用簡單工廠來創建對象,可以減少全局變量創建提高代碼復用率,它的使用場合限制在創建單一對象。
工廠方法模式
作者把工廠方法模式比喻成一張名片。
定義
通過對產品類的抽象使其創建業務主要負責用于創建多類產品的實例。
使用場景
在實際開發中,需求的變更是很正常的,開始需求簡單可以直接創建對象,類似的需求多了可以用簡單工廠方法重構,但是如果需求不停變化,那么不僅要修改工廠函數還要添加類,這樣就沒完了。而工廠方法模式本意是將實際創建對象的工作推遲到子類中。
// 工廠類
var Factory = function(type, content) {
if(this instanceof Factory) {
var s = new this[type](content);
return s;
} else {
// 防止使用者不知道這是一個類,忘了加new操作符創建,導致全局變量污染
return new Factory(type, content);
}
};
Factory.prototype = {
Java: function(content) {
// ...
},
JavaScript: function(content) {
// ...
},
php: function(content) {
// ...
}
};
這樣以后如果想添加其他類,只需要在Factory的原型里添加就可以了。
收獲與總結
對于創建很多類的對象,簡單工廠模式就不適合了,通過工廠模式可以輕松創建多個類的實例對象,而且避免了使用者與對象類之間的耦合,用戶不必關心創建該對象的具體類,只需調用工廠方法即可。
抽象工廠模式
抽象工廠模式讓你感覺出現的都是幻覺。
定義
通過對類的工廠抽象使其業務用于對產品類簇的創建,而不負責某一類產品的實例。
抽象類
抽象類是一種聲明但不能使用的類,當你使用的時候就會報錯。JavaScript中的抽象類不能像傳統面向對象語言那樣輕松地創建,我們可以在類的方法中手動拋出錯誤來模擬抽象類。你可能會想,這樣的類什么都不能做能有什么用?其實它在繼承上是很有用的。
使用場景
抽象工廠模式不能用來創建具體對象,一般用它作為父類類創建一些子類。
// 抽象工廠方法
var VehicleFactory = function(subType, superType) {
// 判斷抽象工廠中是否有該抽象類
if(typeof VehicleFactory[superType] === 'function') {
// 緩存類
function F() {};
// 繼承父類屬性和方法
F.prototype = new VehicleFactory[superType]();
// 將子類構造函數指向子類
subType.constructor = subType;
// 子類原型繼承父類
subType.prototype = new F();
} else {
// 不存在該抽象類拋出錯誤
throw new Error('未創建該抽象類');
}
};
// 小汽車抽象類
VehicleFactory.Car = function() {
this.type = 'car';
};
VehicleFactory.Car.prototype = {
getPrice: function() {
return new Error('抽象方法不能調用')
}
};
// 公交車抽象類
VehicleFactory.Bus = function() {
this.type = 'bus';
};
VehicleFactory.Bus.prototype = {
getPrice: function() {
return new Error('抽象方法不能調用');
}
};
抽象工廠實際上是一個子類繼承父類的方法,在該方法中需要通過傳遞子類以及繼承父類的名稱。
收獲與總結
抽象工廠模式是設計模式中最抽象的一種,也是創建模式中唯一一種抽象化創建模式。該模式創建出的結果不是一個真實的對象實例,而是一個類簇,指定了類的結構。
建造者模式
建造者模式告訴我們分即是合。
定義
將一個復雜對象的構建層與其表示層相互分離,同樣的構建過程可采用不同的表示。
應用場景
現在有一個發布簡歷的需求,就是幫別人在公司網站上發布簡歷,但是這些簡歷有一個需求,除了將興趣愛好以及一些特長發布在頁面里,其他信息如聯系方式等不要發布在網站上,而且每個人想找的工作是可以分類的。這樣一些需求我們需要創建的東西就多了,這時候前面的三種工廠模式都不適合了,這里就可以用建造者模式。
建造者模式和只關心創建結果的工廠模式不同,雖然其目的也是創建一個對象,但是更多關心的是創建這個對象的整個過程。在本例中,我們需要的不僅僅是應聘者的實例還要在創建過程中注意這位應聘者有哪些興趣愛好等。
// 創建一位人類
var Human = function(param) {
// 技能
this.skill = param && param.skill || '保密';
// 興趣愛好
this.hobby = param && param.hobby || '保密';
};
// 類人原型方法
Human.prototype = {
getSkill: function() {
return this.skill;
},
getHobby: function() {
return this.hobby;
}
};
// 實例化姓名類
var Named = function(name) {
var that = this;
// 構造器,解析姓名的姓與名
(function(name, that) {
that.wholeName = name;
if(name.indexOf(' ') > -1) {
that.FirstName = name.slice(0, name.indexOf(' '));
that.FirstName = name.slice(name.indexOf(' '));
}
})(name, that);
};
// 實例化職位類
var Work = function(work) {
var that = this;
// 構造器,通過傳入的職位特征來設置相應職位及描述
(function(work, that) {
switch(work) {
case 'code':
that.work = '工程師';
break;
case 'UI':
case 'UE':
that.work = '設計師';
break;
case 'teach':
that.work = '教師';
break;
default:
that.work = work;
}
})(work, that);
};
// 更換期望的職位
Work.prototype.changeWork = function(work) {
this.work = work;
};
下面來創建一位應聘者
// 應聘者創建類
var Person = function(name, work) {
// 創建應聘者緩存對象
var _person = new Human();
// 創建應聘者姓名解析對象
_person.name = new Named(name);
// 創建應聘者期望職位
_person.work = new Work(work);
// 返回創建的應聘者對象
return _person;
}
收獲與總結
建造者模式和前面幾種創建型設計模式不同,它關心對象的整個創建過程,因此通常將創建對象的類模塊化,這樣使創建類的每一個模塊都可以得到靈活的運用與高質量的復用。這種方式對于整個對象類的拆分無形中增加了結構的復雜性,因此如果對象粒度很小,或者模塊間的復用率很低,不建議使用建造者模式。
原型模式
原型模式是JavaScript語言之魂。
定義
用原型實例指向創建對象的類,使用于創建新的對象的類共享原型對象的屬性以及方法。
使用場景
還是關于子類繼承父類的問題,為了提高性能,對于每次創建的一些簡單的而又有差異化的屬性可以放在構造函數中,將一些消耗資源比較大的方法放在基類的原型中,這樣就可以避免不必要的消耗,這就是原型模式的雛形。
原型模式更多的是用在對象的創建上,比如創建一個實例對象的構造函數比較復雜或者耗時比較長,或者通過創建多個對象來實現。此時最好不要用new關鍵字去復制這些基類,可以通過對這些對象屬性或者方法進行復制來實現創建。首先要有一個原型對象的復制方法。
// 原型對象復制方法
function prototypeExtend() {
var F = function() {},
args = arguments,
i = 0,
len = args.length;
for (; i < len; i++) {
// 遍歷每個模板對象中的屬性
for(var j in args[i]) {
F.prototype[j] = args[i][j];
}
}
// 返回緩存類實例
return new F();
}
企鵝游戲中創建一個企鵝對象,如果沒有企鵝基類,只提供了一些動作模板對象,可以通過實現這些模板對象的繼承來創建一個企鵝實例對象。
var penguin = prototypeExtend({
speed: 20,
swim: function() {
console.log('游泳速度' + this.speed);
},
run: function() {
console.log('奔跑速度' + this.speed);
}
})
這樣通過prototypeExtend創建的就是一個對象,不用再用new去創建一個新的實例對象。
收獲與總結
原型模式實際上也是一種繼承,可以讓多個對象分享同一個原型對象的屬性和方法,這種繼承的實現是不需要創建的,而是將原型對象分享給那些繼承的對象。原型對象更適合在創建復雜的對象時,對于那些需求一直在變化而導致對象結構不停地改變時,將那些比較穩定的屬性與方法共用而提取的繼承的實現。
單例模式
哈哈,讓你感受下一個人的寂寞。
定義
又被稱為單體模式,只允許實例化一次的對象類。有時也可以用一個對象來規劃一個命名空間,井井有條地管理對象上的屬性和方法。
使用場景
單例模式應該是JavaScript中最常見的一種設計模式了,經常為我們提供一個命名空間,來防止不同的人命名變量的沖突。還可以用它來創建一個小型的代碼庫。
var A = {
Util: {
util_method1: function() {},
util_method2: function() {}
},
Tool: {
tool_method1: function() {},
tool_method2: function() {}
},
Ajax: {
ajax_method1: function() {},
ajax_method2: function() {}
}
...
}
如果想使用這個代碼庫,像下面這樣訪問即可:
A.Util.util_method1();
A.Tool.tool_method2();
收獲與總結
單例模式有時也被稱為單體模式,它是只允許實例化一次的對象類,有時這么做也是為了節省系統資源。JavaScript中單例模式經常作為命名空間對象來實現,通過單例對象,我們可以將各個模塊的代碼井井有條地梳理在一起。
結構型設計模式
結構型設計模式關注于如何將類或對象組合成更大、更復雜的結構,以簡化設計。主要有外觀模式,適配器模式,代理模式,裝飾者模式,橋接模式,組合模式和享元模式。
外觀模式
作者把這種模式比喻成一種套餐服務。
定義
為一組復雜的子系統接口提供一個更高級的統一接口,通過這個接口使得對子系統接口的訪問更加容易。在JavaScript中有時也會用于對底層結構兼容性做統一封裝來簡化用戶使用。
使用場景
為頁面文檔document對象添加點擊事件時,如果直接用onclick來綁定事件,那么如果團隊中再有人要為document綁定click事件時,就會把之前綁定的那個時間覆蓋,因為這是DOM0級事件。我們應該用DOM2級事件處理程序提供的addEventListener來實現,然而老版本IE是不支持這個方法的,必須用attachEvent,這樣如果我們寫一個能兼容所有瀏覽器的方式操作起來就會更方便,這時候就可以用到外觀模式。為功能統一但方法不統一的接口提供一個統一的接口。
// 外觀模式實現
function addEvent(dom, type, fn) {
// 對于支持DOM2級事件處理程序的瀏覽器
if(dom.addEventListener) {
dom.addEventListener(type, fn, false);
// 對于不支持addEventListener但支持attachEvent的瀏覽器
} else if(dom.attachEvent) {
dom.attachEvent('on' + type, fn);
} else {
dom['on' + type] = fn;
}
}
解決瀏覽器兼容問題只是外觀模式應用的一部分,很多代碼庫中都是通過外觀模式來封裝多個功能,簡化底層造作方法的。
收獲與總結
當一個復雜的系統提供一系列復雜的接口方法時,為系統的管理方便會造成接口方法的使用及其復雜。通過外觀模式,對接口進行二次封裝可以隱藏其復雜性。
適配器模式
聽到這個是的名字,有沒有想到水管彎彎的場景呢?
定義
將一個類(對象)的接口(方法或者屬性)轉化成另外一個接口,以滿足用戶需求,使類(對象)之間接口的不兼容問題通過適配器得以解決。
使用場景
公司有個活動頁面正在使用公司內部開發的A框架,可是很多新來的同事使用A框架開發新的功能需求時總是感覺很吃力,而且能用的方法有限,為了讓新同事盡快融入項目的開發,可以引入jQuery框架,由于A框架和jQuery框架很像,這樣就可以寫一個適配器而不需要將之前的代碼全用jQuery寫一遍。
適配器模式不僅在編程中很常見,在生活中這種模式也很常見,比如三角插頭充電器對于兩項插頭是不能用的,此時就需要一個三項轉兩項插頭電源適配器,這就是一種適配器模式,其實它就是為了兩個代碼庫所寫的代碼兼容運行而書寫的額外代碼。
JavaScript中適配器模式還能適配兩個代碼庫,適配參數,適配數據,適配服務端數據等。以參數適配為例。
function doSomeThing(name, title, age, color, size, prize){}
記住這些參數的順序是很困難的,所以我們經常是以一個參數對象方式傳入的,如下所示:
/**
* obj.name: name
* obj.title: title
* obj.age: age
* obj.color: color
* obj.size: size
* obj.prize: prize
***/
function doSomeThing(obj){}
然而當調用的時候也不能確定傳遞的參數是否完整,如有一些必須得參數沒有傳入,一些參數有默認值等,這個時候就可以用適配器來適配傳入的參數對象。
function doSomeThing(obj) {
var _adapter = {
name: '雨夜清荷',
title: '設計模式',
age: 24,
color: 'pink',
size: 100,
prize: 50
};
for(var i in _adapter) {
_adapter[i] = obj[i] || _adapter[i];
}
}
收獲與總結
JavaScript中的適配器更多應用在對象之間,為了使對象可用,通常會將對象拆分并重新包裝,這樣就要了解適配器對象的內部結構,這也是與外觀模式的區別所在。
代理模式
有沒有想到牛郎織女鵲橋相會的場景?
定義
由于一個對象不能直接引用另一個對象,所以需要通過代理對象在這兩個對象之間起到中介作用。
使用場景
跨域問題應該是使用代理模式解決的一個最典型的問題。由于用戶模塊上傳的照片量越來越大,導致服務器需要將上傳模塊重新部署到另外一個域中,這就導致了跨域問題。我們可以將相冊頁面和上傳模塊所在的服務器抽象成兩個對象,想讓跨域兩端的對象之間實現通信,就需要找個代理對象來實現他們之間的通信。
代理對象有很多種,簡單一點的如img之類的標簽通過src可以向其他域下的服務器發送請求。不過這類請求是get請求,是單向的,不會有響應數據。另外一種代理對象的形式是通過script標簽。而我們需要的代理對象,是對頁面與瀏覽器間通信的,JSONP就實現了一種代理模式。我們知道src屬性可以實現get請求,因此可以在src指向的url地址上添加一些字段信息,服務器獲取這些字段信息,相應生成一分內容。
// 前端瀏覽器頁面
<script type="text/javascript">
// 回調函數
function jsonpCallBack(res,req) {
console.log(res,req);
}
</script>
<script type="text/javascript" src="http://localhost/test/jsonp.php?callback=jsonp CallBack&data=getJsonPData"></script>
// 另一個域下的服務器請求接口
<?php
/* 后端獲取請求字段數據,并生成返回內容 */
$data = $_GET["data"];
$callback = $_GET["callback"];
echo $callback."('success', '".$data."')";
?>
這種方式可以想象成合理的一只小船,通過小船將你的請求發送給對岸,然后對岸的人們將數據放在小船里為你帶回來。
收獲與總結
代理模式除了在跨域問題中有很多應用外,有時對對象的實例化對資源的開銷很大,如頁面加載初期加載文件有很多,此時能夠延遲加載一些圖片對頁面首屏加載時間收益是很大的,通過代理可以先加載預覽圖片然后再加載開銷大的圖片。
由此可見,代理模式可以解決系統之間耦合度以及系統資源開銷大的問題,通過代理對象可以保護被代理對象,使被代理對象不受外界的影響。
裝飾者模式
顯然房子裝修就是一種典型的裝飾者模式。
定義
在不改變原對象的基礎上,通過對其進行包裝擴展(添加屬性或者方法)使原有對象可以滿足用戶的更復雜需求。
使用場景
靜止是相對的,運動是絕對的,所以沒有一成不變的需求。在實際項目開發中需求總在不斷變化,當原有的功能已經不能滿足用戶的需求時,我們要做的就是在這個基礎上添磚加瓦,設置新功能和屬性來滿足用戶提出的需求,這就是裝飾者模式要做的。
// 裝飾者
var decorator = function(input, fn) {
// 獲取事件源
var input = document.getElementById(input);
// 若事件源已經綁定事件
if(typeof input.onclick === 'function') {
// 緩存事件源原有回調函數
var oldClickFn = input.onclick;
// 為事件源定義新的事件
input.onclick = function() {
// 事件源原有回調函數
oldClickFn();
// 執行事件源新增回調函數
fn();
}
} else {
input.onclick = fn;
}
}
收獲與總結
除了裝飾者模式,適配器模式也可以對原有對象進行擴展,所不同的是適配器進行擴展很多時候是對對象內部結構的重組,因此了解其自身結構是必須的。而裝飾者模式對對象的擴展是一種良性擴展,不用了解其具體實現,只是在外部進行了一次封裝擴展。
橋接模式
作者把這種模式比喻成城市間的公路。
定義
在系統沿著多個維度變化的同時,又不增加其復雜度并已達到解耦。
使用場景
有時候,頁面中一些小小細節的改變常常因邏輯相似而導致大片臃腫的代碼,讓頁面苦澀不堪。現在項目有一個需求,是要把頁面上部的用戶信息添加一些鼠標劃過的特效,但是用戶信息由很多小組件組成,對于用戶名,鼠標劃過直接改變背景色,但是像用戶等級、用戶消息這類部件只能改變里面的數字內容,處理邏輯不太一樣。這樣就需要寫不少代碼,但是又會感覺很冗余。這時候,我們首先要提取共同點,對想的抽象邏輯做抽象提取處理。
對于用戶信息模塊的每一部分鼠標滑過與鼠標離開兩個事件的執行函數有很大一部分是相似的,比如它們都處理每個部件中的某個元素,它們都是處理元素的字體顏色和背景顏色。可以創建下面這樣一個函數,解除this耦合。
function changeColor(dom, color, bg) {
// 設置元素的字體顏色
dom.style.color = color;
// 設置元素的背景顏色
dom.style.background = bg;
}
接下來就是對具體元素綁定時間了,但是僅僅知道元素事件綁定與抽象提取的設置樣式方法changeColor是不夠的,需要用一個方法將他們鏈接起來,這個方法就是橋接方法,這種模式就是橋接模式。就像你開著車去沈陽,那么你就需要找到一條連接北京與沈陽的公路,才能順利往返兩地。
對于事件的橋接方法,可以用一個匿名函數來代替。
var spans = document.getElementsByTagName('span');
spans[0].onmouseover = function() {
changeColor(this, 'red', '#ddd');
}
收獲與總結
橋接模式最主要的特點是將實現層(如元素綁定事件)與抽象層(如修飾頁面UI邏輯)解耦分離,使兩部分可以獨立變化,橋接模式主要是對結構之間的解耦。
組合模式
作者把組合模式比喻成超值午餐,感覺很形象。
定義
又稱部分-整體模式,將對象組合成樹形結構以表示“部分整體”的層級結構。組合模式使得用戶對單個對象和組合對象的使用具有一致性。
使用場景
為強化首頁用戶體驗,項目經理準備在用戶首頁添加一個新聞模塊,當然新聞的內容是根據用戶平時關注的內容挖掘的,因此有的人可能會顯示文字新聞,有的人可能會是圖片新聞等等。
我們先來仔細分析下這個需求,需求中的這些新聞大致可以分為相互獨立的幾種類型,對某類新聞做修改時不會影響到其他類的新聞,這樣可以將每一類新聞抽象成面向對象編程中的一個類,然后在這些新聞類中挑選一些組合成需要的模塊,這時候就可以用組合模式了。
在頁面中,組合模式更常用在創建表單上,比如注冊頁面可能有不同的表單提交模塊。對于這些需求,我們只需要有一個基本的個體,然后通過一定的組合即可實現。
收獲與總結
組合模式能夠給我們提供一個清晰的組成結構,組合對象類通過繼承同一個父類使其具有統一的方法,這樣也方便了統一管理與使用。
享元模式
作者把享元模式比喻成城市公交車,可以仔細思考一番。
定義
運用共享技術有效地支持大量的細粒度的對象,避免對象間擁有相同內容造成多余的開銷。
使用場景
現在有新聞的內容太多,我們有了一個分頁顯示所有新聞的需求。一個簡單直觀的做法就是頁面加載后異步請求新聞數據,然后創建所有條新聞插入頁面中,需要顯示哪一頁就顯示哪一頁。但是這樣做有一個很大的問題,這樣一下子創建幾百條新聞同時插入頁面會造成多頁的開銷嚴重影響網頁的性能。這里的所有新聞都有相似的結構,只是內容不同罷了,對于這種相同結構造成多余開銷的問題,可以用享元模式來解決。
享元模式 主要是對其數據、方法共享分離,將數據和方法分成內部數據、內部方法和外部數據、外部方法。內部方法與內部數據指的是相似或共有的數據和方法,所以將其提取出來減少開銷。上面例子中,所有新聞個體都有共同的結構,應該作為內部數據,而下一頁按鈕綁定的事件則是外部方法。同時為了使用內部數據還需要提供一個操作方法。
var Flyweight = function() {
// 已創建的元素
var created = [];
// 創建一個新聞包裝容器
function create() {
var dom = document.createElement('div');
// 將容器插入新聞列表容器中
document.getElementById('container').appendChild(dom);
// 緩存新創建的元素
created.push(dom);
// 返回創建的新元素
return dom;
}
return {
// 獲取創建新聞元素方法
getDiv: function() {
// 如果已創建的元素小于當前頁元素總個數(5個),則創建
if(created.length < 5) {
return created();
} else {
// 獲取第一個元素,并插入去后面
var div = created.shift();
created.push(div);
return div;
}
}
}
}
上面創建一個享元類,由于每頁只能顯示5條新聞,所以創建5個元素,保存在享元類內部,可以通過getDiv方法來獲取創建的元素。下面就要實現外部數據和外部方法,外部數據就是我們要顯示的所有新聞內容,由于每個內容都不一樣肯定不能共享。首先,我們要根據新聞內容實例化頁面,然后,對下一頁綁定一個點擊事件,顯示下一頁。
var paper = 0,
num = 5,
len = article.length;
// 添加五條新聞
for(var i = 0; i < 5; i++) {
if(article[i])
// 通過享元類獲取創建的元素并寫入新聞內容
Flyweight.getDiv().innerHTML = article[i];
}
// 下一頁按鈕綁定事件
document.getElementById('next_page').onclick = function() {
// 如果新聞內容不足5條則返回
if(article.length < 5) {
return;
}
var n = ++paper * num % len, // 獲取當前頁的第一條新聞索引
j = 0;
// 插入5條新聞
for(; j < 5; j++) {
// 如果存在n+j條則插入
if(article[n + j]) {
Flyweight.getDiv().innerHTML = article[n + j];
// 否則插入起始位置第n+j-len條
} else if(article[n + j - len]) {
Flyweight.getDiv().innerHTML = article[n + j - len];
} else {
Flyweight.getDiv().innerHTML = "";
}
}
}
這樣用享元模式對頁面重構之后每次操作只需要操作5個元素,這樣性能可以提高很多。
收獲與總結
享元模式的應用是為了提高程序的執行效率與系統性能,因此在大型系統開發中應用比較廣泛,可以避免程序中的數據重復。應用時一定要找準內部狀態與外部狀態,這樣才能更合理地提取分離。
行為型設計模式
行為型設計模式用于不同對象之間職責劃分或算法抽象,行為型設計模式不僅僅涉及類和對象,還涉及類或對象之間的交流模式并加以實現。行為型設計模式主要有模板方法模式,觀察者模式,狀態模式,策略模式,職責鏈模式,命令模式,訪問者模式,中介者模式,備忘錄模式,迭代器模式和解釋器模式,這么多的模式真得好好消化一陣子了。
模板方法模式
作者把這種模式比喻成照貓畫虎。
定義
父類中定義一組操作算法骨架,而將一些實現步驟延遲到子類,使得子類可以不改變父類算法結構的同時可重新定義算法中某些實現步驟。
使用場景
提示框歸一化,一個網站有很多頁面,如果每個頁面的彈出框樣式不太一致就會顯得不是很和諧,需要將他們的樣式統一。新手最直觀的想法就是去每個頁面一個個修改,當然這樣的代價是很大的,我們需要寫一個彈出框插件,將這些彈出框封裝好,然后再各個頁面調用即可。這是在這個插件中就可以使用模板方法模式了,不需要重復寫多個樣式。
模板方法模式就是將多個模型抽象畫歸一,從中抽象出一個最基本的模板,這個模板可以作為實體也可以作為抽象對象,其他模塊只需要繼承這個模板方法,也可以擴展某些方法。
打個比方,我們生活中用蛋糕做模具做蛋糕,做出的蛋糕是外形相同的,因為他們都用同一個模具。然而商店里面賣的蛋糕是各式各樣的,這都是對蛋糕的二次加工。我們的需求中基本提示框就是我們抽象出來的模具,其他提示框比這個提示框要多一些功能,我們只需要對他們做一些二次加工就能滿足需求了。
模板方法不僅在歸一化組件時使用,有時候創建頁面時也是很常用的,比如創建三類導航,第一類是基礎的,第二類是多了消息提醒功能的,第三類多了后面顯示網址功能。這也可以用模板方法實現,此時抽象出來的基類是最簡單的基礎導航類。
// 格式化字符串方法
function formateString(str, data) {
return str.replace(/\{#(\w+)#\}/g, function(match, key) {
return typeof data[key] === undefined ? '': data[key]
});
}
// 基礎導航
var Nav = function(data) {
// 基礎導航樣式模板
this.item = '<a href="{#href#}" title="{#title#}">{#name#}</a>';
// 創建字符串
this.html = '';
// 格式化數據
for(var i = 0, len = data.length; i < len; i++) {
this.html += formateString(this.item, data[i]);
}
// 返回字符串數據
return this.html;
}
對于消息提醒導航類,只需額外添加消息提醒組件模板,并與消息提醒組件模板對傳入的網址數據進行裝飾,得到所需的字符串,在調用從基類繼承的方法處理這些字符串即可。
var NumNav = function(data) {
// 消息提醒信息組件模板
var tpl = '<b>{#num#}</b>';
// 裝飾數據
for(var i = data.length - 1; i >= 0; i--) {
data[i].name += data[i].name + formateString(tpl, data[i]);
}
// 繼承基礎導航類
return Nav.call(this, data);
}
收獲與總結
模板方法的核心在于對方法的重用,將核心方法封裝在基類中,讓子類繼承基類的方法,實現基類方法的共享,達到方法共用。子類繼承的方法是可擴展的,這就需要對基類繼承的方法進行重寫。
觀察者模式
作者把這種模式比喻成通信衛星。
定義
又被稱作發布-訂閱模式或消息機制,定義了一種依賴關系,解決了主體對象與觀察者之間功能的耦合。
使用場景
在團隊開發中,經常是一個人負責一個模塊,那么每人負責的模塊之間要如何進行溝通呢?比如你實現一些需求需要添加一些代碼,但是這個需求需要其他模塊配合,但是每個模塊都是不同人寫的,你不想因為新添加的代碼影響到他人實現的功能,這個時候就需要用到觀察者模式了。
觀察者模式就是為了解決主體對象與觀察者之間的耦合。打個比方,目前每個國家都在研發并發射衛星,發射這些衛星是為了監控一些信息,那么它就可以被看做一個觀察者或者說是一個消息系統,如果讓這顆衛星為飛機導航,那么這架飛機就是一個被觀察者或者說是一個主體對象。那么如果地面上的中轉站或者其他飛機需要知道這架飛機的信息,于是每當飛機到達一個地方時就會向衛星發出位子信息,然后衛星又將信息廣播到已經訂閱這架飛機的中轉站,這樣就可以避免一些飛機事故發生。
這時候,觀察者至少需要有兩個方法,一個是接收某架飛機發來的消息,一個是向訂閱的中轉站發送響應消息。但是,并不是每個中轉站都要時刻監控飛機狀態的,所以還需要一個取消注冊的方法。當然這些消息還需要保存,就需要一個保存消息的容器。這時候觀察者雛形就出來了,他有一個消息容器和三個方法,訂閱消息方法,取消訂閱消息方法,發送訂閱消息方法。
var Observer = (function() {
// 防止消息隊列暴露而被篡改,故將消息容器作為靜態私有變量保存
var __messages = {};
return {
// 注冊信息接口
regist: function() {},
// 發布信息接口
fire: function() {},
// 移除信息接口
remove: function() {}
}
})();
下面就是可以自己具體實現這些接口了。
收獲與總結
觀察者模式最主要是解決類或對象之間的耦合,解耦兩個互相依賴的對象,使其依賴于觀察者的消息機制。這樣對于任何一個訂閱者來說,其他訂閱者對象的改變不會影響到自身,其自身既可以是消息的發出者也可以是消息的執行者,這都依賴于調用觀察者對象中的三種方法(訂閱,注銷,發布消息)中的哪一種。
狀態模式
作者把這種模式比喻成超級瑪麗。
定義
當一個對象內部狀態發生改變時,會導致其行為的改變,這看起來像是改變了對像。
使用場景
平時寫代碼的時候經常會遇到要寫很多條件判斷語句的情況,那么怎么減少代碼中的條件判斷語句呢?對于這類分支條件內部獨立結果的管理,可以使用狀態模式,每一種條件作為對象的一種狀態,面對不同的判斷結果,其實就是選擇對象內的一種狀態。
將不同的判斷結果封裝在狀態對象內,然后該狀態對象返回一個可被調用的接口方法,用于調用狀態對象內部的某種方法。
// 投票結果狀態對象
var ResultState = function() {
// 判斷結果保存在內部狀態中
var States = {
// 每種狀態作為一種獨立方法保存
state0: function() {
console.log('這是第一種情況'):
},
state1: function() {
console.log('這是第二種情況'):
},
state2: function() {
console.log('這是第三種情況'):
},
state3: function() {
console.log('這是第四種情況'):
}
}
// 獲取某種狀態并執行對應方法
function show(result) {
States['state' + result] && States['state' + result]();
}
return {
// 返回調用狀態方法接口
show: show
}
}();
想調用第三種結果就可以如下調用
ResultState.show(3);
對于狀態模式,主要目的就是將條件判斷的不同結果轉化為狀態對象的內部狀態,這個內部狀態一般作為狀態對象的私有變量,然后提供一個能夠調用狀態對象內部狀態的接口方法對象即可。
收獲與總結
狀態模式既是解決程序中臃腫的分支判斷語句問題,將每一個分支轉化為一種狀態獨立出來,方便每種狀態的管理又不至于每次只需時遍歷所有分支。
策略模式
作者把這種模式比喻成活諸葛。
定義
將定義的一組算法封裝起來,使其相互之間可以替換。封裝的算法具有一定獨立性,不會隨客戶端變化而變化。
使用場景
年底的時候,公司商品展銷頁都要開展大促銷活動。在圣誕節,一部分商品5折出售,一部分商品8折出售,一部分商品9折出售,到元旦搞個幸運反饋活動,普通用戶滿100返30,高級VIP用戶滿100返50。這個時候上面的狀態模式就不適用了,因為每一天每一個商品只有一種促銷情況,這個時候可以用策略模式。
結構上看,它與狀態模式很像,也是在內部封裝一個對象,然后通過返回的接口對象實現實現對內部對象的調用,不同點是,策略模式不需要管理狀態、狀態間沒有依賴關系、策略之劍可以相互替換、在策略對象內部保存的是相互獨立的一些算法。看看策略對象的實現:
// 價格策略對象
var PriceStrategy = function() {
// 內部算法對象
var strategy = {
// 100返30
return30: function(price) {},
// 100返50
return50: function(price) {},
// 9折
percent90: function(price) {},
// 8折
percent80: function(price) {},
// 5折
percent50: function(price) {},
}
// 策略算法調用接口
return function(algorithm, price) {
return strategy[algorithm] && strategy[algorithm](price);
}
}();
收獲與總結
策略模式主要特色是創建一系列策略算法,每組算法處理業務都是相同的,只是處理的過程或者處理的結果不一樣,所以它們是可以相互替換的,這樣就解決了算法與使用者之間的耦合。
職責鏈模式
作者把這種模式比喻成一個有序車站。
定義
解決請求的發送者與請求的接受者之間的耦合,通過職責鏈上的多個對象對分解請求流程,實現請求在多個對象之間的傳遞,知道最后一個對象完成請求的處理。
使用場景
項目經理準備改善頁面中的輸入驗證與提示交互體驗。如用戶在輸入框輸入信息后,在輸入框的下面提示出一些備選項,當用戶輸入完成后,則要對用戶輸入信息進行驗證等,頁面中很多模塊需要用戶提交信息,為增強用戶體驗,這些輸入框大部分需要具備以上兩種功能。現在需要完成這個需求,但是以后可能要對原有表單交互體驗做一些修改,也就是這是一個半成品需求。這種情況下,我們需要將需求里面需要做的每一件事情獨立出來,這樣完整的需求就變成一個個相互獨立的模塊需求,這樣就不會因為以后需求的改變而影響我們項目的進展,這樣還有利于以后的單元測試。這其實就是一種職責鏈模式。
對于上面的需求,對輸入框綁定事件是第一部分,第二部分是創建xhr進行異步數據獲取,第三部分就是適配響應數據,將接收到的數據格式化成可處理的形式,最后一部分是向組件創建器傳入相應數據生成組件。
收獲與總結
職責鏈模式定義了請求的傳遞方向,通過多個對象對請求的傳遞,實現一個復雜的邏輯操作。因此職責鏈模式將負責的需求顆粒化逐一實現每個最小分內的需求,并將請求順序地傳遞。對于職責鏈上的每一個對象來說,它可能是請求的發起者也可能是請求的接收者,通過這種方式不僅僅簡化原對象的復雜度,而且解決原請求的發起者與原請求的接收者之間的耦合。
命令模式
定義
將請求與實現解耦并封裝成獨立對象,從而使不同的請求對客戶端的實現參數化。
使用場景
現在的需求是要做一個活動頁面,平鋪式的結構,不過頁面的每個模塊都有些相似的地方,比如每個預覽產品圖片區域,都有一行標題,然后標題下面是產品圖片,只是圖片的數量與排列不同。我們需要一種自由創建視圖模塊的方法,有時候創建多張圖片有時候只創建一張圖片,這時候可以試試命令模式。
命令模式是將創建模塊的邏輯封裝在一個對象里,這個對象提供一個參數化的請求接口,通過調用這個接口并傳遞一些參數實現調用命令對象內部中的一些方法。請求部分很簡單,只需要按照給定參數格式書寫指令即可,所以實現部分的封裝才是重點,因為它要為請求部分提供所需方法。
那么哪些對象需要被命令化呢?既然需要動態展示不同模塊,所以創建元素這一需求就是變化的,因此創建元素方法、展示方法應該被命令化。
// 模塊實現模塊
var viewCommand = (function() {
var tpl = {
// 展示圖片結構模塊
product: [
'<div>',.....,'</div>'
].join(''),
// 展示標題結構模塊
title: [
'<div>',.....,'</div>'
].join(''),
},
// 格式化字符串緩存字符串
html = '';
// 格式化字符串
function formateString(str, obj) {}
// 方法集合
var Action = {
// 創建方法
create: function(data, view) {
// 解析數據
if(data.length) {
// 遍歷
for(var i = 0, len = data.length; i < len; i++) {
html += formateString(tpl[view], data[i]);
}
} else {
html += formateString(tpl[view], data);
}
},
// 展示方法
display: function(container, data, vuew) {
// 如果傳入數據
if(data) {
// 根據給的數據創建視圖
this.create(data, view);
}
// 展示模塊
document.getElementById(container).innerHTML = html;
// 展示后清空緩存字符串
html = '';
}
}
// 命令接口
return function excute(msg) {
// 解析命令,如果msg.param不是數組則將其轉化為數組
msg.param = Object.prototype.toString.call(msg.param) === "[object Array]" ? msg.param : [msg.param];
// Action內部調用的方法引用this,此處保證作用域this執行傳入Action
Action[msg.command].apply(Action, msg.param)
}
})();
下面就可以測試這個命令對象了:
var productData = [
{
src: 'command/02.jpg',
text: '綻放的桃花'
},
{
src: 'command/03.jpg',
text: '陽光下的溫馨'
}
],
// 模塊標題數據
titleData = {
title: '夏日里的一片溫馨',
tips: '暖暖的溫情帶給人們家的感覺'
}
// 調用命令對象
viewCommand({
command: 'display',
param: ['title', titleData, 'title']
});
viewCommand({
command: 'create',
param: ['product', productData, 'product']
});
有了命令模式,想創建任何頁面視圖都是一件很簡單的事情。
收獲與總結
命令模式是將執行的命令封裝,解決命令發起者與命令執行者之間的耦合,每一條命令實質上是一個操作。命令的是使用者不必了解命令執行者的命令接口是如何實現的,只需要知道如何調用。
訪問者模式
作者把這種模式比喻成駐華大使。
定義
針對于對象結構中的元素,定義在不改變對象的前提下訪問結構中元素的新方法。
使用場景
用DOM2級事件為頁面中元素綁定事件時,為css設置一些樣式如下:
var bindEvent = function(dom, type, fn) {
if(dom.addEventListener) {
dom.addEventListener(type, fn, false);
} else if(dom.attachEvent) {
dom.attachEvent('on' + type, fn);
} else {
dom['on' + type] = fn;
}
}
var demo = document.getElementById('demo');
bindEvent(demo, 'click', function() {
this.style.background = 'red';
});
這個在IE瀏覽器中會出問題,因為IE的attachEvent事件中this指向的竟然是window而不是這個元素,所以如果想獲取事件對象必須用window.e來獲取。這個問題可以借用訪問者模式來解決。
訪問者模式的思想是我們在不改變操作對象的同時,為它添加新的操作方法,來實現對操作對象的訪問。下面看看IE的實現方式:
function bindIEEvent(dom, type, fn, data) {
var data = data || {};
dom.attachEvent('on' + type, function(e){
fn.call(dom, e, data);
});
};
上面實現方法的核心就是調用call方法,call方法的作用就是更改函數執行時的作用域,這正是訪問者模式的精髓。
收獲與總結
訪問者模式解決數據與數據操作方法之間的耦合,將數據的操作方法獨立于數據,使其可以自由化演變。訪問者更適合那些數據穩定但是數據的操作方法易變的環境下。
中介者模式
作者把這種模式比喻成媒婆,好吧,我笑了這里。
定義
通過中介者對象封裝一系列對象之間的交互,是對象之間不再相互引用,降低他們之間的耦合。有時中介者對象也可以改變對象之間的交互。
使用場景
項目經理準備在用戶首頁上的導航模塊添加一個設置層,讓用戶可以通過設置層來設置導航展開樣式。但是頁面中好多模塊都有導航,這要改起來工作量也很大,上面講的觀察者模式雖然能解決模塊之間的耦合,但是這里我們并沒有需要向設置層發送請求的需求,設置層只是單向控制導航模塊內導航的樣式。這樣的單向通信就可以使用中介者模式。
觀察者模式和中介者模式都是通過消息收發機制實現,不過在觀察者模式中,一個對象既可以是消息的發送者也可以是消息的接收者,而中介者模式中消息的發送方只有一個就是中介者對象,而且中介者對象不能訂閱消息,只有那些活躍對象(訂閱者)才能訂閱中介者消息。
如果用中介者模式來解決上面的問題,那么中介者對象就是設置層模塊對象,它負責向各個導航模塊對象發送用戶設置消息,而各個導航模塊則應該作為消息的訂閱者存在,實現如下:
// 中介者對象
var Mediator = function() {
// 消息對象
var _msg = {};
return {
// 訂閱消息方法,type:消息名稱 action:消息回調函數
register: function(type, action) {
// 如果消息存在
if(_msg[type])
// 存入回調函數
_msg[type].push(action);
else {
// 不存在則建立消息容器
_msg[type] = [];
_msg[type].push(action);
}
},
// 發布消息方法
send: function(type) {
// 如果該消息已經被訂閱
if(_msg[type]) {
// 遍歷已存儲的消息回調函數
for(var i = 0, len = _msg[type].length; i < len; i++) {
// 執行回調函數
_msg[type][i] && _msg[type][i]();
}
}
}
}
}();
這樣就創建了一個中介者對象,下面就可以利用這個中介者對象完成我們的需求了。
收獲與總結
同觀察者模式一樣,中介者模式的主要業務也是通過模塊間或者對象間的復雜通信,來解決模塊間或對象間的耦合。在中介者模式中,訂閱者是單向的,只能是訂閱者而不能是發布者。而消息統一由中介者對象發布。
備忘錄模式
定義
在不破壞對象的封裝性的前提下,在對象之外捕獲并保存該對象內部狀態以便日后對象使用或者對象恢復到以前的某個狀態。
使用場景
在前面提到的新聞頁面中,有上一頁和下一頁的按鈕,頁面的內容是用異步請求獲取的。如果點擊下一頁按鈕接著再點擊上一頁那么之前那一頁又要進行一次異步請求,這是多余的操作。因為第一次已經獲取了數據,不需要再發送多余的請求。這個時候可以用備忘錄模式來緩存請求過的數據。也就是說每次發生請求的時候對當前狀態做一次記錄,將請求到的數據以及對應得頁碼緩存下來,如果之后返回到之前瀏覽過的頁面,直接在緩存中查詢即可,不用發生異步請求。先創建一個新聞緩存器:
// Page備忘錄類
var Page = function() {
// 信息緩存對象
var cache = {};
return function(page, fn) {
// 判斷該頁數據是否在緩存中
if(cache[page]) {
// 顯示該頁內容
showPage(page, cache[page]);
// 執行成功回調函數
fn && fn();
} else {
// 否則異步請求
$.post('./data/getNewsData.php', {
page: page
}, function(res) {
// 成功返回
if(res.errNo == 0) {
showPage(page, res.data);
cache[page] = res.data;
fn && fn();
} else {
// 處理異常
}
})
}
}
}
上面代碼可以看出Page緩存器內部緩存了每次請求回來的新聞數據,這樣以后如果用戶想回看某頁新聞數據就不需要發送不必要的請求了。
收獲與總結
備忘錄模式最主要的任務是對現有的數據或狀態進行緩存,為將類某個時刻使用或恢復做準備。但是當數據量過大時,會嚴重占用系統提供的資源,此時對緩存器的優化是很有必要的,復用率低的數據緩存下來是不值得的。
迭代器模式
作者把這種模式比喻成一個點鈔機。
定義
在不暴露對象內部結構的同時,可以順序地訪問聚合對象內部的元素。
使用場景
迭代器模式主要是解決重復循環迭代的問題,之前接觸過面向對象語言的應該都對迭代器有所了解。迭代器就是用來順序地訪問一個聚合對象內部元素的,它可以簡化我們遍歷操作,就行銀行里的點鈔機,有了它可以大幅度降低我們的點鈔成本。下面創建一個常用的迭代器對象:
var Iterator = function(items, container) {
// 獲取父元素
var container = container && document.getElementById(container) || document,
// 獲取元素
items = container.getElementsByTagName(items),
// 獲取元素長度
length = items.length,
// 當前索引值
index = 0;
// 緩存原生數組splice方法
var splice = [].splice;
return {
// 獲取第一個元素
first: function() {},
// 獲取最后一個元素
second: function() {},
// 獲取前一個元素
pre: function() {},
// 獲取后一個元素
next: function() {},
// 獲取某一個元素
get: function(num) {},
// 對每一個元素執行某一個方法
dealEach: function(fn) {},
// 對某一個元素執行某一個方法
dealItem: function(num, fn) {},
// 排他方式處理某一個元素
exclusive: function() {}
}
}
下面具體實現迭代器里面的這些方法,然后就可以用這個迭代器對象啦。
收獲與總結
通過迭代器我們可以順序地訪問一個聚合對象中的每一個元素。在開發中,迭代器極大簡化了代碼中的循環語句,使代碼結構清晰緊湊。用迭代器去處理一個對象時,只需要提供處理的方法,而不必去關心對象的內部結構,這也解決了對象的使用者與對象內部結構之間的耦合。
解釋器模式
定義
對于一種語言,給出其文法表示,并定義一種解釋器,通過使用這種解釋器來解釋語言中定義的句子。
使用場景
一個頁面中的某些功能好壞有時是靠一定的數據依據支撐的。項目經理想看看用戶對最近新增的功能使用情況,前后端要給出統計數據,然而前端交互統計項中要給出交互元素路徑。這件事情與冒泡事件類似,只不過在這個路徑中還要關心同一層級中當前元素的兄弟元素。比如下面的結構:
<div calss="wrap">
<div class="link-inner">
<a href="#">link</a>
</div>
<div class="button-inner">
<button>text</button>
</div>
</div>
要獲取button相對于class為wrap的div元素的Xpath路徑,那么可以表示為DIV>DIV2>SPAN。
上面對需求的描述是一種文法,描述的是一組規則,現在要做的事實現一個規則解釋器來解釋上面的規則。首先要分析給出的文法,查找他們的相似點,然后該清楚我們要先實現什么再實現什么,基本上問題就能解決了。
收獲與總結
一些描述性語句,幾次功能的提取抽象,形成了一套語法法則,這就是解釋器模式要處理的事情。是否能應用解釋器模式的一條重要準則是能否根據需求解析出一套完整的語法規則,不論該語法規則簡單或是復雜都是必須的。
技巧型設計模式
技巧型設計模式是通過一些特定技巧來解決組件的某些方面的問題,這類技巧一般通過實踐經驗總結得到。這本書中總結了8種技巧型設計模式,分別是鏈模式,委托模式,數據訪問對象模式,節流模式,簡單模板模式,惰性模式,參與者模式和等待者模式。有興趣的同學可以去買書來看哦,這里就不一一解釋了。
架構型設計模式
架構型設計模式是一類框架結構,通過提供一些子系統,指定它們的職責,并將它們條理清晰地組織在一起。現在流行的前端框架都用了這種類型的設計模式。本書總結了6種架構型設計模式,分別是同步模塊模式,異步模塊模式,Widget模式,MVC模式,MVP模式和MVVM模式。
學習設計模式的學習對于我們來說任重而道遠,我們需要在實踐中不斷思考不斷總結。