寫(xiě)在前面
設(shè)計(jì)模式是不分語(yǔ)言的,本文介紹的是14種設(shè)計(jì)模式,幾乎涵蓋了js中涉及到所有設(shè)計(jì)模式,部分設(shè)計(jì)模式代碼實(shí)踐部分也會(huì)分別,用面向?qū)ο笏季S 和 js"鴨子類型"思維 兩種代碼對(duì)比同一種設(shè)計(jì)模式。內(nèi)容較長(zhǎng),讀完定有收獲!
js中的this
跟別的語(yǔ)言大相徑庭的是,JavaScript的this總是指向一個(gè)對(duì)象,而具體指向哪個(gè)對(duì)象是在運(yùn)行時(shí)基于函數(shù)的執(zhí)行環(huán)境動(dòng)態(tài)綁定的,而非函數(shù)被聲明時(shí)的環(huán)境。
- this 的指向,具體到實(shí)際應(yīng)用中,this的指向大致可以分為以下4種。
? 作為對(duì)象的方法調(diào)用。
? 作為普通函數(shù)調(diào)用。
? 構(gòu)造器調(diào)用。
? Function.prototype.call或Function.prototype.apply調(diào)用。下面我們分別進(jìn)行介紹。
- 1.作為對(duì)象方法調(diào)用,this 指向該對(duì)象
var obj = {
a:1,
getA:function(){
console.log(this==obj);//輸入:true
console.log(this.a);//輸出:1
}
}
obj.getA();
- 2.作為普通函數(shù)調(diào)用,指向全局對(duì)象。在瀏覽器中,這個(gè)全局對(duì)象就是window對(duì)象
window.name = 'windowNmae';
var getName = function(){
return this.name;
}
console.log(getName());//輸出: windowNmae
- 3.構(gòu)造器調(diào)用,構(gòu)造器里的this就指向返回的這個(gè)對(duì)象
var MyClass = function(){
this.name = 'MyClass'
};
var obj = new MyClass();
console.log(obj.name);//輸出: MyClass
- Function.prototype.call或Function.prototype.apply調(diào)用,可以動(dòng)態(tài)地改變傳入函數(shù)的this
var obj1 = {
name:'sven',
getName:function(){
return this.name;
}
};
var obj2 = {
name:'anne'
};
console.log(obj1.getName());//輸出: sven
console.log(obj1.getName.call(obj2));//輸出: anne
設(shè)計(jì)模式介紹
1.單利模式(惰性單利)
var Singleton = function(name){
this.name = name
}
Singleton.getSingle = (function(){
var instance = null;
return function(name){
if(!instance){
instance = new Singleton(name);
}
return instance;
}
})();
var singleton1 = Singleton.getSingle('app1');
var singleton2 = Singleton.getSingle('app2');
console.log(singleton1==singleton2);//輸出:true
console.log(singleton1.name,singleton2.name);//輸出:app1 app1
2.策略模式
很多公司的年終獎(jiǎng)是根據(jù)員工的工資基數(shù)和年底績(jī)效情況來(lái)發(fā)放的。例如,績(jī)效為S的人年終獎(jiǎng)有4倍工資,績(jī)效為A的人年終獎(jiǎng)有3倍工資,而績(jī)效為B的人年終獎(jiǎng)是2倍工資。假設(shè)財(cái)務(wù)部要求我們提供一段代碼,來(lái)方便他們計(jì)算員工的年終獎(jiǎng)
- 面向?qū)ο笳Z(yǔ)言思想實(shí)現(xiàn)
// 策略類
var performanceS = function(){}
performanceS.prototype.calculate = function(salary){
return salary * 4;
}
var performanceA = function(){}
performanceA.prototype.calculate = function(salary){
return salary * 3;
}
var performanceB = function(){}
performanceB.prototype.calculate = function(salary){
return salary * 2;
}
// 獎(jiǎng)金類
var Bonus = function(){
this.salary = null;//原始工資
this.strategy = null;//績(jī)效對(duì)應(yīng)的策略對(duì)象
}
//設(shè)置原始工資
Bonus.prototype.setSalary = function(salary){
this.salary = salary;
}
//設(shè)置績(jī)效等級(jí)對(duì)象的策略對(duì)象
Bonus.prototype.setStrategy = function(strategy){
this.strategy = strategy;
}
//取得獎(jiǎng)勵(lì)
Bonus.prototype.getBonus = function(){
//把計(jì)算獎(jiǎng)勵(lì)的操作委托給策略對(duì)象
return this.strategy.calculate(this.salary)
}
//test
var bonus = new Bonus();
bonus.setSalary(10000);
bonus.setStrategy(new performanceS())
console.log(bonus.getBonus());//輸出:40000
bonus.setStrategy(new performanceA())
console.log(bonus.getBonus());//輸出:30000
- js 版本策略模式
var strategies = {
'S':function(salary){
return salary * 4;
},
'A':function(salary){
return salary * 3;
},
'B':function(salary){
return salary * 2;
}
}
var calculateBonus = function(level,salary){
return strategies[level](salary);
}
console.log(calculateBonus('S',10000));//輸出:40000
console.log(calculateBonus('A',10000));//輸出:30000
3.代理模式
代理就是委托別人do,這里我們討論常用的兩種代理
- 3.1虛擬代理
在Web開(kāi)發(fā)中,圖片預(yù)加載是一種常用的技術(shù),如果直接給某個(gè)img標(biāo)簽節(jié)點(diǎn)設(shè)置src屬性,由于圖片過(guò)大或者網(wǎng)絡(luò)不佳,圖片的位置往往有段時(shí)間會(huì)是一片空白。常見(jiàn)的做法是先用一張loading圖片占位,然后用異步的方式加載圖片,等圖片加載好了再把它填充到img節(jié)點(diǎn)里,這種場(chǎng)景就很適合使用虛擬代理。
下面我們來(lái)實(shí)現(xiàn)這個(gè)虛擬代理,首先創(chuàng)建一個(gè)普通的本體對(duì)象,這個(gè)對(duì)象負(fù)責(zé)往頁(yè)面中創(chuàng)建一個(gè)img標(biāo)簽,并且提供一個(gè)對(duì)外的setSrc接口,外界調(diào)用這個(gè)接口,便可以給該img標(biāo)簽設(shè)置src屬性:
var myImage = (function(){
var imgNode = document.createElement('img');
document.body.append(imgNode);
return {
setSrc:function(src){
imgNode.src = src;
}
}
})();
myImage.setSrc('https://himg.bdimg.com/sys/portrait/item/ca253731393330373830351216');
我們把網(wǎng)速調(diào)至5KB/s,然后通過(guò)MyImage.setSrc給該img節(jié)點(diǎn)設(shè)置src,可以看到,在圖片被加載好之前,頁(yè)面中有一段長(zhǎng)長(zhǎng)的空白時(shí)間?,F(xiàn)在開(kāi)始引入代理對(duì)象proxyImage,通過(guò)這個(gè)代理對(duì)象,在圖片被真正加載好之前,頁(yè)面中將出現(xiàn)一張占位的菊花圖loading.gif,來(lái)提示用戶圖片正在加載。代碼如下:
var myImage = (function(){
var imgNode = document.createElement('img');
document.body.append(imgNode);
return {
setSrc:function(src){
imgNode.src = src;
}
}
})();
var proxyImage = (function(){
var img = new Image;
img.onload = function(){
myImage.setSrc(this.src)
}
return {
setSrc:function(src){
myImage.setSrc('file://loading.gif');
img.src = src
}
}
})()
proxyImage.setSrc('https://himg.bdimg.com/sys/portrait/item/ca253731393330373830351216')
- 3.2 緩存代理-計(jì)算乘積
緩存代理可以為一些開(kāi)銷大的運(yùn)算結(jié)果提供暫時(shí)的存儲(chǔ),在下次運(yùn)算時(shí),如果傳遞進(jìn)來(lái)的參數(shù)跟之前一致,則可以直接返回前面存儲(chǔ)的運(yùn)算結(jié)果
var mult = function(){
var a = 1;
for(var i=0;i<arguments.length;i++){
a *= arguments[i];
}
return a;
}
console.log(mult(2,3));//輸出:6
console.log(mult(2,3,4));//輸出:24
現(xiàn)在加入緩存代理函數(shù):
var proxyMult = (function(){
var cache = {};
return function(){
var arg = Array.prototype.join.call(arguments,',');
if(arg in cache){
return cache[arg];
}
return cache[arg] = mult.apply(this,arguments)
}
})();
console.log(proxyMult(1,2,3,4));//輸出:24
console.log(proxyMult(1,2,3,4));//輸出:24
通過(guò)增加緩存代理的方式,mult函數(shù)可以繼續(xù)專注于自身的職責(zé)——計(jì)算乘積,緩存的功能是由代理對(duì)象實(shí)現(xiàn)的。
4.迭代器模式
目前的絕大部分語(yǔ)言都內(nèi)置了迭代器,這里簡(jiǎn)單介紹下倒敘迭代
var reverseEach = function(ary,callback){
for(var i=ary.length-1;i>=0;i--){
callback(i,ary[i])
}
}
reverseEach([0,1,2],function(i,n){
console.log(`i = ${i} n = ${n}`);
/* 輸出
i = 2 n = 2
i = 1 n = 1
i = 0 n = 0
*/
})
5.發(fā)布-訂閱模式
實(shí)際開(kāi)發(fā)經(jīng)常用到的,可以直接拿來(lái)用到項(xiàng)目中
// 全局的發(fā)布訂閱對(duì)象
var EventBus = (function(){
var clientList = {},
listen,
trigger,
remove;
listen = function(key,fn){
if(!clientList[key]){
clientList[key] = [];
}
clientList[key].push(fn);
};
trigger = function(){
var key = Array.prototype.shift.apply(arguments);
var fns = clientList[key];
if(!fns || fns.length==0){
return false;
}
for(var i=0;i<fns.length;i++){
fns[i].apply(this,arguments)
}
};
remove = function(key,fn){
var fns = clientList[key];
if(!fns){
return false;
}
if(!fn){
fns&&(fns.length=0);
}else {
for(var i=fns.length-1;i<=0;i--){
if(fn==fns[i]){
fns.splice(i,1)
}
}
}
};
return {
listen:listen,
trigger:trigger,
remove:remove
}
})()
var callback = function(price){
console.log(`價(jià)格 = ${price}`)
}
EventBus.listen('sq88',callback);
EventBus.trigger('sq88',20000);
// EventBus.remove('sq88',callback);
EventBus.trigger('sq88',20000);
6. 命令模式
命令模式最常見(jiàn)的應(yīng)用場(chǎng)景是:有時(shí)候需要向某些對(duì)象發(fā)送請(qǐng)求,但是并不知道請(qǐng)求的接收者是誰(shuí),也不知道被請(qǐng)求的操作是什么。此時(shí)希望用一種松耦合的方式來(lái)設(shè)計(jì)程序,使得請(qǐng)求發(fā)送者和請(qǐng)求接收者能夠消除彼此之間的耦合關(guān)系。
var closeDoorCommmand = {
execute: function(){
console.log('關(guān)門(mén)');
}
}
var openPcCommand = {
execute: function(){
console.log('開(kāi)電腦');
}
}
var openQQCommand = {
execute: function(){
console.log('登錄QQ');
}
}
var MacroCommand = function(){
return {
commandsList:[],
add:function(command){
this.commandsList.add(command);
},
execute:function(){
for(var i=0;i<this.commandsList.length;i++){
var command = this.commandsList[i];
command.execute();
}
}
}
}
var macroCommand = MacroCommand();
macroCommand.add(closeDoorCommmand)
macroCommand.add(openPcCommand)
macroCommand.add(openQQCommand)
macroCommand.execute();
7.組合模式
在程序設(shè)計(jì)中,也有一些和“事物是由相似的子事物構(gòu)成”類似的思想。組合模式就是用小的子對(duì)象來(lái)構(gòu)建更大的對(duì)象,而這些小的子對(duì)象本身也許是由更小的“孫對(duì)象”構(gòu)成的。

8.模板方法模式
在模板方法模式中,子類實(shí)現(xiàn)中的相同部分被上移到父類中,而將不同的部分留待子類來(lái)實(shí)現(xiàn)。這也很好地體現(xiàn)了泛化的思想。
var Beverage = function(){}
Beverage.prototype.boilWater = function(){
console.log('把是煮沸');
}
Beverage.prototype.brew = function(){
throw new Error('子類必須重寫(xiě)brew方法');
}
Beverage.prototype.pourInCup = function(){
throw new Error('子類必須重寫(xiě)pourInCup方法');
}
Beverage.prototype.customeerWantsCondiments = function(){
return true;//默認(rèn)需要調(diào)料
}
Beverage.prototype.init = function(){
this.boilWater();
this.brew();
this.pourInCup();
if(this.customeerWantsCondiments()){
this.addComponent();
}
}
var CoffeeWithHook = function(){};
CoffeeWithHook.prototype = new Beverage();
CoffeeWithHook.prototype.brew = function(){
console.log('用沸水沖泡咖啡');
}
CoffeeWithHook.prototype.pourInCup = function(){
console.log('把咖啡倒進(jìn)杯子');
}
CoffeeWithHook.prototype.customeerWantsCondiments = function(){
return window.confirm('請(qǐng)問(wèn)需要調(diào)料嗎?');
}
var coffeeWithHook = new CoffeeWithHook();
coffeeWithHook.init();
9.享元模式
元模式的核心是運(yùn)用共享技術(shù)來(lái)有效支持大量細(xì)粒度的對(duì)象。
享元模式的目標(biāo)是盡量減少共享對(duì)象的數(shù)量,關(guān)于如何劃分內(nèi)部狀態(tài)和外部狀態(tài),下面的幾條經(jīng)驗(yàn)提供了一些指引。? 內(nèi)部狀態(tài)存儲(chǔ)于對(duì)象內(nèi)部。? 內(nèi)部狀態(tài)可以被一些對(duì)象共享。? 內(nèi)部狀態(tài)獨(dú)立于具體的場(chǎng)景,通常不會(huì)改變。? 外部狀態(tài)取決于具體的場(chǎng)景,并根據(jù)場(chǎng)景而變化,外部狀態(tài)不能被共享。這樣一來(lái),我們便可以把所有內(nèi)部狀態(tài)相同的對(duì)象都指定為同一個(gè)共享的對(duì)象。而外部狀態(tài)可以從對(duì)象身上剝離出來(lái),并儲(chǔ)存在外部。
假設(shè)有個(gè)內(nèi)衣工廠,目前的產(chǎn)品有50種男式內(nèi)衣和50種女士?jī)?nèi)衣,為了推銷產(chǎn)品,工廠決定生產(chǎn)一些塑料模特來(lái)穿上他們的內(nèi)衣拍成廣告照片。正常情況下需要50個(gè)男模特和50個(gè)女模特,然后讓他們每人分別穿上一件內(nèi)衣來(lái)拍照。不使用享元模式的情況下,在程序里也許會(huì)這樣寫(xiě):
var Model = function(sex,underwear){
this.sex = sex;
this.underwear = underwear;
}
Model.prototype.takePhoto = function(){
console.log(`sex = ${this.sex} underwear = ${this.underwear}`);
}
for(var i=0;i<=50;i++){
var maleModel = new Model('male','underwear' + i);
maleModel.takePhoto();
}
for(var i=0;i<=50;i++){
var femaleModel = new Model('female','underwear' + i);
femaleModel.takePhoto();
}
要得到一張照片,每次都需要傳入sex和underwear參數(shù),如上所述,現(xiàn)在一共有50種男內(nèi)衣和50種女內(nèi)衣,所以一共會(huì)產(chǎn)生100個(gè)對(duì)象。如果將來(lái)生產(chǎn)了10000種內(nèi)衣,那這個(gè)程序可能會(huì)因?yàn)榇嬖谌绱硕嗟膶?duì)象已經(jīng)提前崩潰。下面我們來(lái)考慮一下如何優(yōu)化這個(gè)場(chǎng)景。雖然有100種內(nèi)衣,但很顯然并不需要50個(gè)男模特和50個(gè)女模特。其實(shí)男模特和女模特各自有一個(gè)就足夠了,他們可以分別穿上不同的內(nèi)衣來(lái)拍照。
var Model = function(sex){
this.sex = sex;
}
Model.prototype.takePhoto = function(){
console.log(`sex = ${this.sex} underwear = ${this.underwear}`);
}
var maleModel = new Model('male');
var femaleModel = new Model('female');
for(var i=0;i<50;i++){
maleModel.underwear = 'underwear' + i;
maleModel.takePhoto();
}
for(var i=0;i<50;i++){
femaleModel.underwear = 'underwear' + i;
femaleModel.takePhoto();
}
10.職責(zé)鏈模式
職責(zé)鏈模式的定義是:使多個(gè)對(duì)象都有機(jī)會(huì)處理請(qǐng)求,從而避免請(qǐng)求的發(fā)送者和接收者之間的耦合關(guān)系,將這些對(duì)象連成一條鏈,并沿著這條鏈傳遞該請(qǐng)求,直到有一個(gè)對(duì)象處理它為止。
image.png
- 異步的職責(zé)鏈
而在現(xiàn)實(shí)開(kāi)發(fā)中,我們經(jīng)常會(huì)遇到一些異步的問(wèn)題,比如我們要在節(jié)點(diǎn)函數(shù)中發(fā)起一個(gè)ajax異步請(qǐng)求,異步請(qǐng)求返回的結(jié)果才能決定是否繼續(xù)在職責(zé)鏈中passRequest。
var Chain = function(fn){
this.fn = fn;
this.successor = null;
};
Chain.prototype.setNextSuccessor = function(successor){
return this.successor = successor;
}
Chain.prototype.passRequest = function(){
var ret = this.fn.apply(this,arguments);
if(ret == 'nextSuccessor'){
return this.successor && this.successor.passRequest.apply(this.successor,arguments);
}
return ret;
}
Chain.prototype.next = function(){
return this.successor && this.successor.passRequest.apply(this.successor,arguments);
}
var fn1 = new Chain(function(){
console.log(1);
return 'nextSuccessor'
})
var fn2 = new Chain(function(){
console.log(2);
var self = this;
setTimeout(function(){
self.next();
},1000);
})
var fn3 = new Chain(function(){
console.log(3);
})
fn1.setNextSuccessor(fn2).setNextSuccessor(fn3);
fn1.passRequest();
11.中介者模式
中介者模式的作用就是解除對(duì)象與對(duì)象之間的緊耦合關(guān)系。增加一個(gè)中介者對(duì)象后,所有的相關(guān)對(duì)象都通過(guò)中介者對(duì)象來(lái)通信,而不是互相引用,所以當(dāng)一個(gè)對(duì)象發(fā)生改變時(shí),只需要通知中介者對(duì)象即可。中介者使各對(duì)象之間耦合松散,而且可以獨(dú)立地改變它們之間的交互。中介者模式使網(wǎng)狀的多對(duì)多關(guān)系變成了相對(duì)簡(jiǎn)單的一對(duì)多關(guān)系

12.裝飾器模式
這種給對(duì)象動(dòng)態(tài)地增加職責(zé)的方式稱為裝飾者(decorator)模式。裝飾者模式能夠在不改變對(duì)象自身的基礎(chǔ)上,在程序運(yùn)行期間給對(duì)象動(dòng)態(tài)地添加職責(zé)。跟繼承相比,裝飾者是一種更輕便靈活的做法,這是一種“即用即付”的方式
Function.prototype.before = function(beforeFn){
var that = this;//保存原函數(shù)的引用
return function(){//返回包含了原函數(shù)和新函數(shù)的“代理”函數(shù)
beforeFn.apply(this,arguments);//執(zhí)行新函數(shù),且保證this不被劫持,函數(shù)接收的參數(shù)
return that.apply(this,arguments);//執(zhí)行原函數(shù)并返回原函數(shù)的執(zhí)行結(jié)果,且保證this不被劫持
}
}
Function.prototype.after = function(afterFn){
var that = this;
return function(){
var ret = that.apply(this,arguments);
afterFn.apply(this,arguments);
return ret;
}
}
function myClick(){
console.log('myclick');
}
myClick();
myClick.before(function(){
console.log('前')
}).after(()=>console.log('后'))();
13. 狀態(tài)模式
狀態(tài)模式的關(guān)鍵是區(qū)分事物內(nèi)部的狀態(tài),事物內(nèi)部狀態(tài)的改變往往會(huì)帶來(lái)事物的行為改變。
點(diǎn)燈程序 (弱光 --> 強(qiáng)光 --> 關(guān)燈)循環(huán)
// 關(guān)燈
var OffLightState = function(light) {
this.light = light;
};
// 弱光
var WeakLightState = function(light) {
this.light = light;
};
// 強(qiáng)光
var StrongLightState = function(light) {
this.light = light;
};
var Light = function(){
/* 開(kāi)關(guān)狀態(tài) */
this.offLight = new OffLightState(this);
this.weakLight = new WeakLightState(this);
this.strongLight = new StrongLightState(this);
/* 快關(guān)按鈕 */
this.button = null;
};
Light.prototype.init = function() {
var button = document.createElement("button"),
self = this;
this.button = document.body.appendChild(button);
this.button.innerHTML = '開(kāi)關(guān)';
this.currentState = this.offLight;
this.button.click = function() {
self.currentState.buttonWasPressed();
}
};
// 讓抽象父類的抽象方法直接拋出一個(gè)異常(避免狀態(tài)子類未實(shí)現(xiàn)buttonWasPressed方法)
Light.prototype.buttonWasPressed = function() {
throw new Error("父類的buttonWasPressed方法必須被重寫(xiě)");
};
Light.prototype.setState = function(newState) {
this.currentState = newState;
};
/* 關(guān)燈 */
OffLightState.prototype = new Light(); // 繼承抽象類
OffLightState.prototype.buttonWasPressed = function() {
console.log("關(guān)燈!");
this.light.setState(this.light.weakLight);
}
/* 弱光 */
WeakLightState.prototype = new Light();
WeakLightState.prototype.buttonWasPressed = function() {
console.log("弱光!");
this.light.setState(this.light.strongLight);
};
/* 強(qiáng)光 */
StrongLightState.prototype = new Light();
StrongLightState.prototype.buttonWasPressed = function() {
console.log("強(qiáng)光!");
this.light.setState(this.light.offLight);
};
14. 適配器模式
var renderMap = function(map){
if(map.show instanceof Function){
map.show();
}
}
var googleMap = {
show:function(){
console.log('開(kāi)始渲染谷歌地圖');
}
}
var baiduMap = {
display:function(){
console.log('開(kāi)始渲染百度地圖')
}
}
var baiduMapAdapter = {
show:function(){
return baiduMap.display();
}
}
renderMap(googleMap);
renderMap(baiduMapAdapter);
