JavaScript 設(shè)計(jì)模式介紹+實(shí)戰(zhàn)(14種js常用設(shè)計(jì))

寫(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
  1. 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)成的。

image.png

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)系

image.png

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);
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容