聊一聊常見設(shè)計模式的 JavaScript 實現(xiàn)

開門見山

我們都知道 javascript 是一種基于原型的弱類型語言,擁有動態(tài)數(shù)據(jù)類型,靈活多變。因此,相比于傳統(tǒng)的 javac++ 來說, javascript 面向?qū)ο蟮脑O(shè)計模式會有點牽強(qiáng)。

但這也并不妨礙我們學(xué)習(xí)使用 javascript 來了解設(shè)計模式思想及其設(shè)計理念。因為在我看來,這屬于程序員的 “內(nèi)功” ,只有 “內(nèi)功” 修煉得當(dāng),才能更好的學(xué)習(xí) “上乘武功”。

更何況,現(xiàn)在很多框架的源碼都引入了非常多的設(shè)計模式思想,例如 發(fā)布訂閱模式,單例模式,工程模式等比比皆是。

因此,想要學(xué)習(xí)框架源碼,編寫高質(zhì)量,易維護(hù)的代碼,設(shè)計模式的學(xué)習(xí)是必不可少的。今天我們就聊一聊 JavaScript 中一些常見的設(shè)計模式。

單例模式 (Singleton Pattern)

單例模式,顧名思義就是只有一個實例,并且它自己負(fù)責(zé)創(chuàng)建自己的對象,這個類提供了一種訪問其唯一的對象的方式,可以直接訪問,不需要實例化該類的對象。

代碼示例

// 單例模式
const Singleton = (function () {
    let __instance = null;

    const Singleton = function () {
        if (__instance) return __instance;
        __instance = this;
        this.init();
        return __instance;
    }

    Singleton.prototype.init = function () {
        console.log('Singleton init completed!');
    }

    Singleton.getInstance = function () {
        if (__instance) return __instance;
        __instance = new Singleton()
        return __instance;
    }

    return Singleton;
})();


const s1 = Singleton.getInstance();
const s2 = new Singleton();

console.log(s1 === s2); // true

利用 IIFE 的方式構(gòu)造 __instance 實例,并提供了獲取實例的方法,并保存起來,每次訪問時,如果已經(jīng)初始化則不需要重新創(chuàng)建,直接返回 __instance 即可,保證只有一個實例。

優(yōu)缺點

單例模式的優(yōu)點在于在創(chuàng)建后在內(nèi)存中只存在一個實例,能保證訪問一致性,并且反復(fù)創(chuàng)建銷毀實例來說節(jié)約內(nèi)存和 cpu 資源。

單例模式的缺點也很明顯,因為只有一個實例,也不太需要實例化過程,因此拓展性不好。

適用場景

比較適合項目中需要一個公共的狀態(tài),并通過單例也確保訪問一致性的時候。例如,許多 UI 框架 中的全局 Loading 組件。

工廠模式 (Factory Pattern)

工廠模式,聽名字就應(yīng)該可以大概猜到,是根據(jù)不同的輸入來創(chuàng)建同一類對象的。主要是為了將對象的創(chuàng)建與對象的實現(xiàn)分離。

代碼示例

// 工廠模式

/* 工廠類 */
function CreateElement(type) {
    switch (type) {
        case 'Input':
            return new Input()
        case 'DIV':
            return new Div()
        default:
            throw new Error('當(dāng)前沒有這個產(chǎn)品')
    }
}

/* 產(chǎn)品類 */
function Input() {
    return document.createElement('input')
}

function Div() {
    return document.createElement('div')
}

const input = new CreateElement('Input');
const div = new CreateElement('DIV');


console.log(input)      // input
console.log(div)        // div

上述通過一個簡單的例子演示了工廠模式的創(chuàng)建過程,我們知道有一個大工廠類,負(fù)責(zé)生產(chǎn)產(chǎn)品,只要通過傳遞不同的類型,可以實例化出不同的對象。

優(yōu)缺點

工程模式將對象的創(chuàng)建和實現(xiàn)進(jìn)行了分離,代碼結(jié)構(gòu)清晰,使得代碼高度解耦,具有很多良好的封裝,我們無需知道創(chuàng)建對象的過程就可以得到想要的對象,拓展性也強(qiáng)。

工廠模式的缺點在于,工廠類集中了所有實例的創(chuàng)建邏輯,它所能創(chuàng)建的類只能是事先考慮到的,如果需要添加新的類,則就需要改變工廠類。隨著產(chǎn)品類的不斷增多,代碼的維護(hù)性就越差。

適用場景

當(dāng)對象的創(chuàng)建比較復(fù)雜,而訪問者無需知道創(chuàng)建的具體流程,我們可以考慮使用工廠模式。典型應(yīng)用,如 VueReact 中創(chuàng)建虛擬 DOMcreateElement 函數(shù)。Vue-Router 的設(shè)計,根據(jù)不同的 mode 創(chuàng)建不同的路由實例。

建造者模式(Builder Pattern)

建造者模式,將一個復(fù)雜對象的構(gòu)建與表示分離,使得同樣的構(gòu)建過程可以創(chuàng)建不同的表示。在工程模式中,我們不關(guān)心創(chuàng)建過程,直接得到一個完成的對象。而建造者模式中,我們關(guān)心對象的創(chuàng)建過程,將復(fù)雜對象模塊化,使得每個模塊都可以復(fù)用。

代碼示例

// 建造者模式

/* 建造者 */
function ComputerBuilder(brand) {
    this.brand = brand;
}


ComputerBuilder.prototype.buildCPU = function (type) {
    switch (type) {
        case 'inter':
            this.cpu = 'inter 處理器';
            break;
        case 'AMD':
            this.cpu = 'AMD 處理器';
            break;
    }
    return this;
}

ComputerBuilder.prototype.buildMemory = function (mSize) {
    this.mSize = '內(nèi)存' + mSize + 'G';
    return this;
}

ComputerBuilder.prototype.buildDisk = function (dSize) {
    this.dSize = '硬盤' + dSize + 'G';
    return this;
}


/* 廠家,負(fù)責(zé)組裝 */
function computerDirector(brand, type, mSize, dSize) {
    const _computer = new ComputerBuilder(brand);
    _computer.buildCPU(type)
        .buildMemory(mSize)
        .buildDisk(dSize);
    return _computer;
}

const com = computerDirector('聯(lián)想', 'inter', 16, 500);

console.log(com); // ComputerBuilder {brand: "聯(lián)想", cpu: "inter 處理器", mSize: "內(nèi)存16G", dSize: "硬盤500G"}

上述我們通過生產(chǎn)電腦的例子,描述了建造者模式的構(gòu)建過程,我們的部件都是由一個個類創(chuàng)建出來的,最后進(jìn)行組裝完成整個對象的。如果后期需要拓展組件,只需要在建造者上增加對應(yīng)的方法,再適當(dāng)修改鏈?zhǔn)秸{(diào)用即可。

優(yōu)缺點

建造者模式適用于構(gòu)建復(fù)雜的、需要分步驟構(gòu)建的對象,可以將構(gòu)建過程分離,分步驟進(jìn)行。優(yōu)點顯而易見,具有很好的拓展性,很高的復(fù)用性。

如果對象之間差異過大,復(fù)用性不高的話不建議使用這種模式,否則創(chuàng)建過程中會導(dǎo)致代碼比較亂,復(fù)雜度過高,顯得有些強(qiáng)行建造了。

適用場景

建造者模式適用于可以通過不同的部件組裝得到不同完整產(chǎn)品的場景,可以將代碼最小程度的拆分,利于后期維護(hù)。例如,你封裝一個公共彈窗,里面涉及有標(biāo)題,內(nèi)容,按鈕,文字等,但也不都是必須的,你可以在需要的時候去構(gòu)建他們。

代理模式(Proxy Pattern)

代理模式,是給某一個對象提供一個代理對象,并由代理對象控制對原對象的引用,防止訪問者直接訪問目標(biāo)對象,從而對目標(biāo)對象起到一種間接保護(hù)的作用,類似我們?nèi)粘I钪械母鞣N中介及微商。

ps: ES6 新增的 Proxy 可以非常方面快捷的幫助我們對一個對象進(jìn)行代理,想要了解 Proxy 的同學(xué)可參考我之前的寫的 初探 Vue3.0 中的一大亮點——Proxy !

代碼示例

// 代理模式

// 目標(biāo)
function sendMsg(msg) {
    console.log(msg);
}

// 代理
function ProxyMsg(msg) {
    if (!msg) {
        console.log('msg is empty.')
        return
    }

    msg = '我要發(fā)送的數(shù)據(jù)是:' + msg;

    sendMsg(msg);
}

ProxyMsg('您好!');    // 我要發(fā)送的數(shù)據(jù)是:您好!

上述用一個發(fā)送消息的例子來描述了代理模式的工作原理,我們不直接通過 sendMsg 方法而是通過 ProxyMsg 方法來發(fā)送消息。這樣做的好處是可以在發(fā)送消息之前對一些不合法的消息進(jìn)行過濾,對合法的內(nèi)容進(jìn)行二次包裝。

優(yōu)缺點

代理模式的優(yōu)點在于,代理對象作為訪問者與目標(biāo)對象之間橋梁,對目標(biāo)對象起到保護(hù)的作用。可以很方面的拓展代理對象,并不直接干涉目標(biāo)對象,一定程度上降低了系統(tǒng)的耦合度。

另一方面,額外新增的代理對象無異于增加了整個系統(tǒng)的復(fù)雜度,造成請求處理速度變慢,增加了系統(tǒng)的維護(hù)成本,在使用前需要酌情考慮。

適用場景

隨著前端的不斷發(fā)展,代理模式在前端領(lǐng)域的使用場景還是很多的。

典型的應(yīng)用就是攔截器,項目中 axios 數(shù)據(jù)請求中的 interceptor 攔截器,以及一些權(quán)限校驗的中間件等。另外像目前流行的 vue 框架,其中的數(shù)據(jù)響應(yīng)式就是利用了這一思想,不同的是 vue2 采用的是 Object.defineProperty , 而 vue3 采用的是 Proxy

享元模式(Flyweight Pattern)

享元模式,字面解釋,享就是共享,元就是元素,公共部分。

因此,享元模式就是通過共享技術(shù)實現(xiàn)相同或相似對象的重用,主要用于減少創(chuàng)建對象的數(shù)量,以減少內(nèi)存占用和提高性能。

代碼示例

// 享元對象
function Shape(shape) {
    this.shape = shape;
}

Shape.prototype.draw = function () {
    console.log(`畫了一個 ${this.shape}`)
}

// 享員工廠
const ShapeFactory = (function () {
    const dataMap = {};
    return {
        getShapeContext(shape) {
            // 如果存在,則直接返回
            if (dataMap[shape]) return dataMap[shape];
            else {
                // 沒有就創(chuàng)建,并保存當(dāng)前shape的實例
                const instance = new Shape(shape);
                dataMap[shape] = instance
                return instance;
            }
        }
    }
})();

const rect = ShapeFactory.getShapeContext('rect');
const circle = ShapeFactory.getShapeContext('circle');

rect.draw();     // 畫了一個 rect
circle.draw();   // 畫了一個 circle

上述代碼,我們用來一個繪畫的例子,通過享元工廠去創(chuàng)建不同類型的 "畫筆" 對象,并保存在我們的工廠函數(shù)中,下次使用的時候則不需要重新創(chuàng)建,直接從 map 中讀取即可。這種方式,相比于傳統(tǒng)的用的時候去 new 創(chuàng)建在數(shù)據(jù)量大的時候會節(jié)約很多內(nèi)存。

優(yōu)缺點

享元模式最大的優(yōu)點就在于它可以極大的減少了系統(tǒng)中對象的創(chuàng)建,降低內(nèi)存的使用,加快了運(yùn)行速度,提高了運(yùn)行效率。

提高效率的同時也暴露出其缺點,共享對象的創(chuàng)建,銷毀等都需要增加額外的邏輯,會使整個系統(tǒng)的邏輯變得復(fù)雜,代碼不容易閱讀,維護(hù)的成本增加。

適用場景

享元模式比較適合項目中大量使用了相同或相似對象,可以共享資源時可以考慮。

其實在前端開發(fā)設(shè)計中還是比較常見的,例如我們所熟知和使用的 事件委托 經(jīng)行事件綁定,就是利用了享元模式的原理,我們并不是給每個元素綁定事件,而是為其父元素綁定一個事件,根據(jù)事件參數(shù) event 來判斷。

另外 nodejs 中所使用的數(shù)據(jù)庫連接池,一些緩存服務(wù)器的設(shè)計等是利用這個原理。

適配器模式(Adapter Pattern)

適配器模式,作為兩個不兼容的接口之間的橋梁,目的就是通過適配器的轉(zhuǎn)換解決類(對象)之間接口不兼容的問題,從而使得原本不兼容的接口可以兼容現(xiàn)有的需求。

與早些年傳統(tǒng)的萬能充電器的作用類似。

代碼示例

// 適配器模式

// 百度地圖 api
const baiduMap = {
    show: function () {
        console.log('開始渲染百度地圖')
    }
}


// 高德地圖 api
const AMap = {
    render: function () {
        console.log('開始渲染高德地圖')
    }
}


// 適配器
const baiduAdapter = {
    render: function () {
        return baiduMap.show()
    }
}


function renderMap(map) {
    if (typeof map.render === 'function') {
        map.render()
    }
}

renderMap(AMap);            // 開始渲染高德地圖
renderMap(baiduAdapter);    // 開始渲染百度地圖

上述代碼中演示了適配器模式的原理,我們之前用的是高德地圖 ,如今我們也要接入百度地圖,二者的 api 的渲染方式不同,為了解決不兼容的問題,我們構(gòu)造了一個 baiduAdapter 適配器,這樣我們就可以適用同樣的接口完成不同地圖的渲染。

優(yōu)缺點

適配器模式相對來說是一種簡單的設(shè)計模式,目的就是為了兼容舊的代碼。因此,它的優(yōu)點也很明顯,就是不用大面積更改以前的舊的代碼邏輯,使得原有的邏輯可以復(fù)用,拓展性強(qiáng),靈活性強(qiáng),可以隨時隨地更改或刪除不同的適配器,而不會造成重大的影響。

適配器模式的缺點自然而然就是,多的適配器會增加系統(tǒng)的復(fù)雜度,會使得系統(tǒng)的代碼變得十分松散,凌亂,代碼的的可閱讀性大大折扣。如大規(guī)模使用適配器導(dǎo)致代碼變得凌亂松散時,可以考慮重構(gòu)。

適用場景

適配器模式有點 亡羊補(bǔ)牢 的意思,如果現(xiàn)有的接口已經(jīng)能夠正常工作,那就永遠(yuǎn)不會用上適配器模式。但隨著公司業(yè)務(wù)的發(fā)展,也許現(xiàn)在好好工作的接口,未來的某天卻不再適用于新系統(tǒng),這時候就得考慮適用適配器模式,為其重新賦能。

裝飾器模式 (Decorator Pattern)

裝飾器模式,在不改變原對象的基礎(chǔ)上,對其添加屬性或方法來進(jìn)行拓展,使原有對象可以具有更多功能,而不影響原有的對象結(jié)構(gòu)。與繼承相比裝飾者是一種更輕便靈活的做法。

psES7 關(guān)于裝飾器 @Decorator 已經(jīng)在草案中,我們項目中所使用的裝飾器需借助第三方工具轉(zhuǎn)義,等到時候標(biāo)準(zhǔn)定下來后就可放心使用了。

代碼示例

  const btn = document.querySelector('#btn');

  // 原綁定事件
  btn.onclick = function () {
      console.log('按鈕被點擊了')
  }


  // 新增統(tǒng)計
  function ajaxToServer() {
      console.log('數(shù)據(jù)統(tǒng)計')
  }

  // 裝飾器函數(shù)
  function decorator(target, eventName, cb) {
      const originFn = target['on' + eventName];
      originFn && originFn()
      cb && cb();
  }

  decorator(btn, 'click', ajaxToServer)

以一點按鈕的點擊事件為例,原點擊事件只是打印 按鈕被點擊了 ,現(xiàn)在需要再點擊的時候調(diào)用 ajaxToServer 做數(shù)據(jù)統(tǒng)計,我們通過一個 decorator 裝飾器函數(shù)先將原始綁定的事件緩存起來,再添加我們 ajaxToServer 回調(diào)即可,一來沒有影響到原始代碼的改動,二來后期如果需要新增,按部就班即可。

優(yōu)缺點

裝飾器的有點在于我們不需要關(guān)心原對象的實現(xiàn),裝飾者和被裝飾者之間不會相互耦合,就可以拓展原對象的方法,可維護(hù)性好,并且裝飾器還可以復(fù)用,使得對象的拓展更加靈活。

由于裝飾器的靈活性,因此隨著裝飾器的增多,會導(dǎo)致系統(tǒng)復(fù)雜度增加,尤其是多級裝飾器,會導(dǎo)致代碼錯誤定位困難繁瑣,對于不熟悉這個模式的開發(fā)人員難以理解。

適用場景

裝飾器模式適用于需要動態(tài)拓展對象或類的方法,或者需要對一些功能進(jìn)行排列組合,完成復(fù)雜工功能的時候可以考慮裝飾器模式。

我們在使用vue ,或者scss的時候,有時候會用到 mixinsmixins的原理就類似于裝飾器。

外觀模式 (Facade Pattern)

外觀模式的本質(zhì)是封裝交互,簡化調(diào)用,它的做法是隱藏了系統(tǒng)的復(fù)雜性,將子系統(tǒng)的一組接口封裝起來,給使用者提供了一個統(tǒng)一的高層接口,減少了客戶端與子系統(tǒng)之間的耦合性。

JavaScript中外觀模式常常用于解決瀏覽器兼容性問題以及源碼中的一些函數(shù)重載,很多主流的庫,如 jQuerylodash 等都有涉及。

代碼示例

// 事件綁定
function addEvent(element, type, fn) {
    if (element.addEventListener) {      // 支持 DOM2 級事件處理方法的瀏覽器
        element.addEventListener(type, fn, false)
    } else if (element.attachEvent) {    // 不支持 DOM2 級但支持 attachEvent
        element.attachEvent('on' + type, fn)
    } else {
        element['on' + type] = fn        // 都不支持的瀏覽器
    }
}

// 阻止事件冒泡
function cancelBubble(event) {
    if (event.stopPropagation) {
        event.stopPropagation()
    } else {                    // IE 下
        event.cancelBubble = true
    }
}

// axios 中 getDefaultAdapter
function getDefaultAdapter() {
  var adapter;
  // Only Node.JS has a process variable that is of [[Class]] process
  if (typeof process !== 'undefined' && Object.prototype.toString.call(process) === '[object process]') {
    // For node use HTTP adapter
    adapter = require('./adapters/http');
  } else if (typeof XMLHttpRequest !== 'undefined') {
    // For browsers use XHR adapter
    adapter = require('./adapters/xhr');
  }
  return adapter;
}

上述的代碼片段都是演示了外觀模式的特點,對于用戶端來說都是統(tǒng)一的調(diào)用,但是接口內(nèi)部卻根據(jù)傳參不同,或者運(yùn)行環(huán)境不同等做了對應(yīng)的處理,簡化了客戶端的使用。

優(yōu)缺點

外觀模式的優(yōu)點在于使用者不必關(guān)系子系統(tǒng)的具體實現(xiàn),通過統(tǒng)一的接口調(diào)用就能達(dá)到效果,降低了使用者和系統(tǒng)模塊之間的耦合性,增加了可維護(hù)性和可擴(kuò)展性。

由于外觀模式是將一組子系統(tǒng)的接口進(jìn)行整合,所以它的缺點就很明顯,在系統(tǒng)內(nèi)部擴(kuò)展子系統(tǒng)時 , 容易產(chǎn)生風(fēng)險。

適用場景

外觀模式在很多開源作品中屢見不鮮,例如上述提到的 jQuerylodash 等,實際上我們開發(fā)也會經(jīng)常用到。它比較適合將復(fù)雜的系統(tǒng)進(jìn)行分層,讓外觀模塊成為每層的入口,簡化層與層之間調(diào)用。或者說當(dāng)我們需要通過一個單獨(dú)的函數(shù)或方法來訪問一系列的函數(shù)或方法調(diào)用時,為了使代碼更容易跟蹤管理或者更好的維護(hù)時,可以考慮適用外觀模式。

組合模式 (Composite Pattern)

組合模式是將一系列對象組合成樹形結(jié)構(gòu),以表示 “部分-整體” 的層次結(jié)構(gòu),使用者只需統(tǒng)一地使用組合結(jié)構(gòu)中的所有對象,而不需要關(guān)心它究竟是組合對象還是單個對象。它主要體現(xiàn)了整體與部分的關(guān)系,其典型的應(yīng)用就是樹形結(jié)構(gòu)。

代碼示例

// 創(chuàng)建部門
function createApartment(name) {
    return {
        name,
        _children: [],
        add(target) {
            this._children.push(target);
            return this;
        },
        show(cb) {
            this._children.forEach(function (child) {
                child.show(cb)
            })
        }
    }
}

// 創(chuàng)建員工
function createEmp(num, name) {
    return {
        num,
        name,
        show(cb) {
            cb(this)
        }
    }
}

// 創(chuàng)建部門
const techApartment = createApartment('技術(shù)部');

// 創(chuàng)建子部門
const proApartment = createApartment('產(chǎn)品組'),
    devApartment = createApartment('開發(fā)組');

techApartment.add(proApartment).add(devApartment);

proApartment.add(createEmp(100, '張三'))
    .add(createEmp(101, '李四'))

techApartment.add(createEmp(201, '小劉'))
    .add(createEmp(202, '小王'))
    .add(createEmp(203, '小陳'))
    .add(createEmp(204, '小亮'))

// 遍歷
techApartment.show(function (item) {
    console.log(`工號:${item.num},姓名:${item.name}`)
})

/***
    工號:100,姓名:張三
    工號:101,姓名:李四
    工號:201,姓名:小劉
    工號:202,姓名:小王
    工號:203,姓名:小陳
    工號:204,姓名:小亮
***/

上述我們同通過一個部門的組織架構(gòu)圖來展示了什么是組合模式,可以發(fā)現(xiàn)組合對象和單個子對象具有相同的接口和數(shù)據(jù)結(jié)構(gòu),一次來保證操作一致,我們在遍歷整個 techApartment 對象時,如果當(dāng)前對象是沒有子對象,則自身會做處理,否則會傳遞到下一個子對象中處理,以此完成整個遞歸遍歷。

優(yōu)缺點

組合模式的組合對象和單個子對象具有同樣的接口,所以無論調(diào)用的是組合對象還是葉子對象調(diào)用方式上沒有差別,外部調(diào)用非常方便。拓展性良好,新增節(jié)點會很方便,也不會影響到其他的對象。

隨著節(jié)點的增加,組合模式也暴露出其不足,過多的節(jié)點會導(dǎo)致整個樹狀結(jié)構(gòu)非常復(fù)制,層級嵌套深,內(nèi)存占用較高,導(dǎo)致系統(tǒng)整體性能下降。

適用場景

如果對象組織呈樹形結(jié)構(gòu),操作樹中對象的方法比較類似時可以考慮適用組合模式。常見的比如組織架構(gòu)圖,文件目錄,以及熟悉的 vue 中的 createElement 方法等都采用的組合模式這種設(shè)計理念。

橋接模式(Bridge Pattern)

橋接模式是為了將抽象部分與實現(xiàn)部分分離,使抽象部分和實現(xiàn)部分都可以獨(dú)立的變化而不會互相影響,實現(xiàn)二者的解耦,從而降低了代碼的耦合性,提高了代碼的擴(kuò)展性。

代碼示例

// 橋接方法
function addEvent(ele, eventName, fn) {
    document.querySelector(ele).addEventListener(eventName, fn, false);
}

// 具體業(yè)務(wù)
addEvent('#btn', 'click', function () {
    console.log('hello world');     // hello world
})

上述通過一個簡單的事件監(jiān)聽器的例子來展示了橋接模式的工作原理,橋接方法 addEvent 它內(nèi)部不實現(xiàn)具體的業(yè)務(wù)邏輯,只是抽象出一個方法,它就充當(dāng)了了 DOM 元素與其具體事件綁定的一個橋梁,要實現(xiàn)具體的業(yè)務(wù)邏輯只要給橋接函數(shù)傳遞參數(shù)即可。

優(yōu)缺點

橋接模式分離了抽象和實現(xiàn)部分,將實現(xiàn)層(DOM 元素事件具體邏輯)和抽象層(綁定方法)解耦,使用者不需要關(guān)心細(xì)節(jié)的實現(xiàn),只需要方便快捷的使用即可,提高了代碼的拓展性。

橋接模式的弊端在于需要很好地抽象出橋接方法與業(yè)務(wù)邏輯,具有一定的局限性,另外橋接模式會引入額外的代碼,增加系統(tǒng)的復(fù)雜度。

適用場景

如果開發(fā)中遇到部分系統(tǒng)的復(fù)用性大,且各個部件有獨(dú)立的變化維度,就可以考慮引入橋接模式,實現(xiàn)代碼的分層。常見的如同上述的事件監(jiān)聽器,動態(tài)更新 dom 的樣式,以及 ajax 請求封裝等。

發(fā)布-訂閱模式 (Publish-Subscribe Pattern)

發(fā)布-訂閱模式,它定義了一種一對多的關(guān)系,當(dāng)一個對象的狀態(tài)發(fā)生改變時,所有依賴于它的對象都將得到通知,使得它們能夠自動更新。

代碼示例

// 事件監(jiān)聽器
const Emitter = (function () {
    const _events = {};
    return {
        // 事件綁定
        on(type, cb) {
            if (!_events[type]) {
                _events[type] = [];
            }
            if (typeof cb === "function") {
                _events[type].push(cb);
            } else {
                throw  new Error('參數(shù)類型必須為函數(shù)')
            }
        },
        // 事件解綁
        off(type, cb) {
            if (!_events[type] || !_events[type].includes(cb)) return;
            // 移除事件監(jiān)聽
            _events[type].map((fn, index) => {
                 if (fn === cb) {
                    _events[type].splice(index, 1)
                }
            })
        },
        emit(type, ...args) {
            if (!_events[type]) return;
            _events[type].forEach(cb => cb(...args))
        }
    }
})();

// 事件訂閱
Emitter.on('change', data => console.log(`我是第一條信息:${data}`))
Emitter.on('change', data => console.log(`我是第二條信息:${data}`))

// 事件發(fā)布
Emitter.emit('change', '參數(shù)')

上述我們通過發(fā)布訂閱模式實現(xiàn)了一個簡單的事件監(jiān)聽器,可以通過 on 方法監(jiān)聽某一事件,之后通過 emit 方法去分發(fā),所有監(jiān)聽該事件的函數(shù)都會被依次執(zhí)行,這就是發(fā)布訂閱模式基本工作原理。

優(yōu)缺點

發(fā)布訂閱模式最大的特點就是發(fā)布者和訂閱者之間完全解耦:發(fā)布者不需要訂閱者是誰,只需要更新的時候遍歷所以訂閱該消息的訂閱者即可。訂閱者也不需要時時關(guān)注發(fā)布者的動態(tài),當(dāng)有消息更新時會自動接受。因此,可以將事件處理中心封裝起來,統(tǒng)一管理,獨(dú)立運(yùn)行。

發(fā)布訂閱模式的缺點在于,訂閱者會增加內(nèi)存消耗,及時后續(xù)沒有觸發(fā),也會常駐內(nèi)存中。隨著訂閱者的增多,系統(tǒng)復(fù)雜度會增加,代碼運(yùn)行效率、資源消耗會變大。另外,發(fā)布者與訂閱者完全解耦,會導(dǎo)致代碼追蹤起來比較困難。

適用場景

發(fā)布訂閱模式特別適用于要實現(xiàn)一對多關(guān)聯(lián)的場景。日常生活中我們訂閱的公眾號,關(guān)注的明星微博,今日頭條的新聞等,他們都會在有新消息的時候第一時間推送給你。而實際開發(fā)中,vue 的數(shù)據(jù)響應(yīng)式,瀏覽器的 DOM 事件綁定等也都是這個原理。

策略模式 (Strategy Pattern)

策略模式就是將一系列算法封裝起來,并使它們相互之間可以替換。被封裝起來的算法具有獨(dú)立性,外部不可改變其特性。它的目的就是將算法的使用與算法的實現(xiàn)分離開來,有效避免代碼中很多if-else的條件語句。

代碼示例

// 校驗規(guī)則
const strategyMap = {
    // 校驗手機(jī)號
    isMobile(mobile) {
        return /^1\d{10}$/.test(mobile);
    },
    // 校驗是否必填
    isRequired(str) {
        return str.replace(/(^\s*)|(\s*$)/g, "") !== "";
    }
};

// 校驗方法
function validate(formData) {
    let valid;

    for (let key in formData) {
        const val = formData[key].value;
        const rules = formData[key].rules;

        for (let i = 0; i < rules.length; i++) {
            const result = strategyMap[rules[i]['rule']].call(null, val);
            if (!result) {
                valid = {
                    errField: key,
                    errValue: val,
                    errMsg: rules[i]['message']
                }
                break;
            }
        }

        if (valid) return valid;
    }

    return valid;
}


// form 表單校驗
const formData = {
    mobile: {
        value: '1380000000',
        rules: [
            {rule: 'isRequired', message: '手機(jī)號碼不能為空'},
            {rule: 'isMobile', message: '手機(jī)號碼格式不正確'},
        ]
    }
}

// 獲取校驗結(jié)果
const valid = validate(formData)
if (!valid) {
    console.log('校驗通過')
} else {
    console.log(valid)   
    // {errField: "mobile", errValue: "1380000000", errMsg: "手機(jī)號碼格式不正確"}
}

上述用了一個非常經(jīng)典的表單校驗來展示了策略模式的應(yīng)用,我們可以事先將一些校驗規(guī)則即一些策略算法放到一個 map 中,通過 validate 方法來完成對復(fù)雜表單的校驗,相比于傳統(tǒng)的 if-else 判斷看起來簡單明了的多,也可以大大提高開發(fā)效率。

優(yōu)缺點

通過上述例子,可以發(fā)現(xiàn)策略模式可以將一個個算法封裝起來,提高代碼復(fù)用率,減少代碼冗余;它可看作為 if/else 判斷的另一種表現(xiàn)形式,在達(dá)到相同目的的同時,極大的減少了代碼量以及代碼維護(hù)成本。另外策略模式中各個策略之間相互獨(dú)立,互不影響,使得它具有良好的可擴(kuò)展性。

策略模式的缺點在于各個策略相互獨(dú)立,因此一些復(fù)雜的算法邏輯無法共享,造成資源的浪費(fèi)。另一方面,我們必須實現(xiàn)定義好種種策略,且使用者必須事先了解這些策略方能靈活運(yùn)用,在一定程度上來講,對于使用者來說不是很方便。

適用場景

策略模式比較適合實現(xiàn)某一個功能有多種方案可以選擇,自由切換的場景,或者是有時需要多重條件判斷,可以使用策略模式來規(guī)避多重條件判斷的情況。前端典型應(yīng)用就是許多開源框架中 form 表單的動態(tài)校驗,以及電商系統(tǒng)中不同優(yōu)惠券的對應(yīng)不同的邏輯等。

狀態(tài)模式 (State Pattern)

狀態(tài)模式定義是一個對象在其內(nèi)部狀態(tài)改變時對應(yīng)的改變它的行為,對象看起來似乎修改了它的類。其意思就是說 對象行為是基于狀態(tài)來改變的,內(nèi)部的狀態(tài)轉(zhuǎn)化,導(dǎo)致了行為表現(xiàn)形式不同。

其主要是用來解決系統(tǒng)中復(fù)雜對象的狀態(tài)轉(zhuǎn)換以及不同狀態(tài)下行為的封裝問題。

代碼示例

// 正常狀態(tài)
function NormalState() {
    this.handleChange = function (context) {
        console.log('正常狀態(tài)')
        context.state = new ColorfulState()
    }
}

// 彩燈狀態(tài)
function ColorfulState() {
    this.handleChange = function (context) {
        console.log('彩燈狀態(tài)')
        context.state = new CloseState()
    }
}

// 關(guān)閉狀態(tài)
function CloseState() {
    this.handleChange = function (context) {
        console.log('關(guān)閉狀態(tài)')
        context.state = new NormalState()
    }
}

// 燈
function Light(state) {
    this.state = state;
    this.switch = function () {
        this.state.handleChange(this)
    }
}

// 設(shè)置燈光初始為關(guān)閉
const light = new Light(new CloseState());

setInterval(() => {
    light.switch()      
}, 1000)

// 關(guān)閉狀態(tài)-->正常狀態(tài)-->彩燈狀態(tài)-->關(guān)閉狀態(tài)...

我們通過生活中一個客廳的燈光狀態(tài)來掩飾狀態(tài)模式的是如何工作的,我們把每個狀態(tài)定義成一個類,并且把每個狀態(tài)所對應(yīng)的功能處理封裝起來,這樣選擇不同狀態(tài)的時候,其實就是在選擇不同的狀態(tài)處理類,由于狀態(tài)是在運(yùn)行期被改變的,因此行為也會在運(yùn)行期根據(jù)狀態(tài)的改變而改變,看起來,同一個對象,在不同的運(yùn)行時刻,行為是不一樣的,就像是類被修改了一樣。

優(yōu)缺點

很明顯,狀態(tài)模式之間的狀態(tài)都是一個個不同的類,相比于 switch-caseif-else 語句的使用,狀態(tài)模式結(jié)構(gòu)很清晰,拓展性很好,需要添加狀態(tài)時,只需要在新增一個狀態(tài)類即可,并且狀態(tài)切換提供了統(tǒng)一的接口,外部的調(diào)用無需知道類內(nèi)部如何實現(xiàn)狀態(tài)和行為的變換,具有很好的封裝性。

狀態(tài)模式的缺點在于,每個狀態(tài)都有對應(yīng)的類,因此系統(tǒng)會引入了很多的類,這樣導(dǎo)致系統(tǒng)中類的個數(shù)增加,維護(hù)成本變高。

適用場景

如果系統(tǒng)的代碼中有多分支的條件語句,且這些分支依賴于某個對象的狀態(tài)時,可以考慮使用狀態(tài)模式來將分支的處理分散到單獨(dú)的狀態(tài)類中,來實現(xiàn)狀態(tài)和行為的分離。

前端的Promise就是一個典型的狀態(tài)模式。前端處理 ajax 請求返回不同的 status 時對應(yīng)的處理邏輯,就可以考慮使用狀態(tài)模式了。

命令模式 (Command Pattern)

命令模式,就是將一系列操作的指令封裝起來,根據(jù)客戶端不同的請求參數(shù)執(zhí)行的對應(yīng)的方法,本質(zhì)上是對方法調(diào)用的封裝,但它可以使請求發(fā)送者和接收者消除彼此之間的耦合關(guān)系。

代碼示例

const Manager = (function () {
    // 命令
    const commander = {
        open: function () {
            console.log('打開電視')
        },
        close: function () {
            console.log('關(guān)閉電視')
        },
        change: function (channel) {
            console.log('更換頻道 ' + channel)
        }
    }

    return {
        // 執(zhí)行命令
        exec: function (cmd) {
            const args = [].splice.call(arguments, 1)
            commander[cmd] && commander[cmd](args)
        }
    }
})();

Manager.exec('open')        // 打開電視
Manager.exec('change', 10)  // 更換頻道 10
Manager.exec('close')       // 關(guān)閉電視

上述代碼以一種簡單的方式展示了命令模式的基本用法,我們是先定義好一些命令,并暴露出一個執(zhí)行命令的 exec 方法,使用者就可以通過 Manager.exec 傳遞不同的命令參數(shù)來達(dá)到執(zhí)行不同命令的效果。

優(yōu)缺點

上面的代碼很明顯就可以看出,命令模式中命令的請求和命令的執(zhí)行兩者完全解耦,因此系統(tǒng)的可擴(kuò)展性良好,加入新的命令不會影響原有邏輯,而且復(fù)用性很強(qiáng),可以被任何請求者使用,不關(guān)心請求者是誰。

命令模式的缺點在于,一是使用者要事先了解有哪些命令方能正常使用,二是隨著命令的不斷增加系統(tǒng)會變得很膨脹,復(fù)雜性會隨之增加。

適用場景

命令模式比較適合于需要發(fā)布一些命令,但不清楚接受者和請求的操作,即只用知道發(fā)布了一個指令就行,具體做什么誰來做不用關(guān)心。常見的 GUI 編程中基本都采用這種模式,前端比較典型應(yīng)用如,富文本編輯器中的各種按鈕,canvas 動畫中各種指令操作等。

總結(jié)

我們上面用了很大的篇幅總結(jié)了常見的一些的設(shè)計模式在 JavsScript 中的實現(xiàn),雖然在 js 中有些設(shè)計模式看起來有些不盡人意,但這卻不是我們所要關(guān)注的核心,我們真正需要關(guān)心的是這些設(shè)計模式的理念、它所要解決的問題。

設(shè)計模式對于我們學(xué)習(xí)框架源碼,做一些前端架構(gòu)是非常有幫助的,只有真正了解了它的思想,明白它所能解決的問題,才能讓我們在開發(fā)中少走彎路,寫出高質(zhì)量的代碼。

也希望閱讀到這的你,繼續(xù)加油,時刻保持一顆學(xué)習(xí)的心態(tài),繼續(xù)在程序員這條道路上摸爬滾打!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,316評論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,481評論 3 415
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,241評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,939評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 71,697評論 6 409
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,182評論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,247評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,406評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,933評論 1 334
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,772評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,973評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,516評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,209評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,638評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,866評論 1 285
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,644評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 47,953評論 2 373