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

開門見山

我們都知道 javascript 是一種基于原型的弱類型語言,擁有動態(tài)數(shù)據(jù)類型滴须,靈活多變舌狗。因此,相比于傳統(tǒng)的 java扔水,c++ 來說痛侍, javascript 面向對象的設計模式會有點牽強。

但這也并不妨礙我們學習使用 javascript 來了解設計模式思想及其設計理念魔市。因為在我看來主届,這屬于程序員的 “內功” 赵哲,只有 “內功” 修煉得當,才能更好的學習 “上乘武功”君丁。

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

因此簸喂,想要學習框架源碼毙死,編寫高質量,易維護的代碼喻鳄,設計模式的學習是必不可少的。今天我們就聊一聊 JavaScript 中一些常見的設計模式确封。

單例模式 (Singleton Pattern)

單例模式除呵,顧名思義就是只有一個實例,并且它自己負責創(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 的方式構造 __instance 實例泛豪,并提供了獲取實例的方法,并保存起來侦鹏,每次訪問時诡曙,如果已經(jīng)初始化則不需要重新創(chuàng)建,直接返回 __instance 即可略水,保證只有一個實例价卤。

優(yōu)缺點

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

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

適用場景

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

工廠模式 (Factory Pattern)

工廠模式五续,聽名字就應該可以大概猜到洒敏,是根據(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('當前沒有這個產(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)建過程凶伙,我們知道有一個大工廠類,負責生產(chǎn)產(chǎn)品它碎,只要通過傳遞不同的類型函荣,可以實例化出不同的對象。

優(yōu)缺點

工程模式將對象的創(chuàng)建和實現(xiàn)進行了分離扳肛,代碼結構清晰傻挂,使得代碼高度解耦,具有很多良好的封裝挖息,我們無需知道創(chuàng)建對象的過程就可以得到想要的對象金拒,拓展性也強。

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

適用場景

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

建造者模式(Builder Pattern)

建造者模式珊擂,將一個復雜對象的構建與表示分離圣勒,使得同樣的構建過程可以創(chuàng)建不同的表示。在工程模式中摧扇,我們不關心創(chuàng)建過程圣贸,直接得到一個完成的對象。而建造者模式中扛稽,我們關心對象的創(chuàng)建過程吁峻,將復雜對象模塊化,使得每個模塊都可以復用。

代碼示例

// 建造者模式

/* 建造者 */
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 = '內存' + mSize + 'G';
    return this;
}

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


/* 廠家用含,負責組裝 */
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: "內存16G", dSize: "硬盤500G"}

上述我們通過生產(chǎn)電腦的例子矮慕,描述了建造者模式的構建過程,我們的部件都是由一個個類創(chuàng)建出來的啄骇,最后進行組裝完成整個對象的痴鳄。如果后期需要拓展組件,只需要在建造者上增加對應的方法缸夹,再適當修改鏈式調用即可痪寻。

優(yōu)缺點

建造者模式適用于構建復雜的、需要分步驟構建的對象虽惭,可以將構建過程分離橡类,分步驟進行。優(yōu)點顯而易見芽唇,具有很好的拓展性顾画,很高的復用性。

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

適用場景

建造者模式適用于可以通過不同的部件組裝得到不同完整產(chǎn)品的場景虾标,可以將代碼最小程度的拆分寓盗,利于后期維護。例如璧函,你封裝一個公共彈窗傀蚌,里面涉及有標題,內容蘸吓,按鈕善炫,文字等,但也不都是必須的库继,你可以在需要的時候去構建他們箩艺。

代理模式(Proxy Pattern)

代理模式,是給某一個對象提供一個代理對象宪萄,并由代理對象控制對原對象的引用艺谆,防止訪問者直接訪問目標對象,從而對目標對象起到一種間接保護的作用拜英,類似我們日常生活中的各種中介及微商静汤。

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

代碼示例

// 代理模式

// 目標
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ā)送消息之前對一些不合法的消息進行過濾抹估,對合法的內容進行二次包裝缠黍。

優(yōu)缺點

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

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

適用場景

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

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

享元模式(Flyweight Pattern)

享元模式,字面解釋宙拉,享就是共享宾尚,元就是元素,公共部分谢澈。

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

代碼示例

// 享元對象
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)建缎谷,并保存當前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é)約很多內存春感。

優(yōu)缺點

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

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

適用場景

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

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

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

適配器模式(Adapter Pattern)

適配器模式,作為兩個不兼容的接口之間的橋梁鹅心,目的就是通過適配器的轉換解決類(對象)之間接口不兼容的問題吕粗,從而使得原本不兼容的接口可以兼容現(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 的渲染方式不同议泵,為了解決不兼容的問題,我們構造了一個 baiduAdapter 適配器桃熄,這樣我們就可以適用同樣的接口完成不同地圖的渲染先口。

優(yōu)缺點

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

適配器模式的缺點自然而然就是,多的適配器會增加系統(tǒng)的復雜度划栓,會使得系統(tǒng)的代碼變得十分松散兑巾,凌亂,代碼的的可閱讀性大大折扣茅姜。如大規(guī)模使用適配器導致代碼變得凌亂松散時闪朱,可以考慮重構。

適用場景

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

裝飾器模式 (Decorator Pattern)

裝飾器模式,在不改變原對象的基礎上计维,對其添加屬性或方法來進行拓展袜香,使原有對象可以具有更多功能,而不影響原有的對象結構鲫惶。與繼承相比裝飾者是一種更輕便靈活的做法蜈首。

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

代碼示例

  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)在需要再點擊的時候調用 ajaxToServer 做數(shù)據(jù)統(tǒng)計踩寇,我們通過一個 decorator 裝飾器函數(shù)先將原始綁定的事件緩存起來,再添加我們 ajaxToServer 回調即可六水,一來沒有影響到原始代碼的改動俺孙,二來后期如果需要新增辣卒,按部就班即可。

優(yōu)缺點

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

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

適用場景

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

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

外觀模式 (Facade Pattern)

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

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

代碼示例

// 事件綁定
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)一的調用丢胚,但是接口內部卻根據(jù)傳參不同,或者運行環(huán)境不同等做了對應的處理受扳,簡化了客戶端的使用携龟。

優(yōu)缺點

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

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

適用場景

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

組合模式 (Composite Pattern)

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

代碼示例

// 創(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('技術部');

// 創(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折柠,姓名:小亮
***/

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

優(yōu)缺點

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

隨著節(jié)點的增加百宇,組合模式也暴露出其不足考廉,過多的節(jié)點會導致整個樹狀結構非常復制,層級嵌套深携御,內存占用較高昌粤,導致系統(tǒng)整體性能下降。

適用場景

如果對象組織呈樹形結構因痛,操作樹中對象的方法比較類似時可以考慮適用組合模式婚苹。常見的比如組織架構圖,文件目錄鸵膏,以及熟悉的 vue 中的 createElement 方法等都采用的組合模式這種設計理念膊升。

橋接模式(Bridge Pattern)

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

代碼示例

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

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

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

優(yōu)缺點

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

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

適用場景

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

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

發(fā)布-訂閱模式聚假,它定義了一種一對多的關系,當一個對象的狀態(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ā)布者不需要訂閱者是誰,只需要更新的時候遍歷所以訂閱該消息的訂閱者即可舶掖。訂閱者也不需要時時關注發(fā)布者的動態(tài)球昨,當有消息更新時會自動接受。因此眨攘,可以將事件處理中心封裝起來主慰,統(tǒng)一管理,獨立運行鲫售。

發(fā)布訂閱模式的缺點在于共螺,訂閱者會增加內存消耗,及時后續(xù)沒有觸發(fā)情竹,也會常駐內存中藐不。隨著訂閱者的增多,系統(tǒng)復雜度會增加秦效,代碼運行效率雏蛮、資源消耗會變大。另外棉安,發(fā)布者與訂閱者完全解耦底扳,會導致代碼追蹤起來比較困難。

適用場景

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

策略模式 (Strategy Pattern)

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

代碼示例

// 校驗規(guī)則
const strategyMap = {
    // 校驗手機號
    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: '手機號碼不能為空'},
            {rule: 'isMobile', message: '手機號碼格式不正確'},
        ]
    }
}

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

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

優(yōu)缺點

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

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

適用場景

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

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

狀態(tài)模式定義是一個對象在其內部狀態(tài)改變時對應的改變它的行為辑莫,對象看起來似乎修改了它的類学歧。其意思就是說 對象行為是基于狀態(tài)來改變的,內部的狀態(tài)轉化各吨,導致了行為表現(xiàn)形式不同枝笨。

其主要是用來解決系統(tǒng)中復雜對象的狀態(tài)轉換以及不同狀態(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()
    }
}

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

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

// 設置燈光初始為關閉
const light = new Light(new CloseState());

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

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

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

優(yōu)缺點

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

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

適用場景

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

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

命令模式 (Command Pattern)

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

代碼示例

const Manager = (function () {
    // 命令
    const commander = {
        open: function () {
            console.log('打開電視')
        },
        close: function () {
            console.log('關閉電視')
        },
        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')       // 關閉電視

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

優(yōu)缺點

上面的代碼很明顯就可以看出偷俭,命令模式中命令的請求和命令的執(zhí)行兩者完全解耦浪讳,因此系統(tǒng)的可擴展性良好,加入新的命令不會影響原有邏輯涌萤,而且復用性很強淹遵,可以被任何請求者使用,不關心請求者是誰负溪。

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

適用場景

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

總結

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

設計模式對于我們學習框架源碼镜廉,做一些前端架構是非常有幫助的弄诲,只有真正了解了它的思想,明白它所能解決的問題娇唯,才能讓我們在開發(fā)中少走彎路齐遵,寫出高質量的代碼。

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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末伶授,一起剝皮案震驚了整個濱河市断序,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌糜烹,老刑警劉巖违诗,帶你破解...
    沈念sama閱讀 218,682評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異疮蹦,居然都是意外死亡诸迟,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,277評論 3 395
  • 文/潘曉璐 我一進店門愕乎,熙熙樓的掌柜王于貴愁眉苦臉地迎上來阵苇,“玉大人,你說我怎么就攤上這事感论∩骶粒” “怎么了?”我有些...
    開封第一講書人閱讀 165,083評論 0 355
  • 文/不壞的土叔 我叫張陵笛粘,是天一觀的道長。 經(jīng)常有香客問我湿硝,道長薪前,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,763評論 1 295
  • 正文 為了忘掉前任关斜,我火速辦了婚禮示括,結果婚禮上,老公的妹妹穿的比我還像新娘痢畜。我一直安慰自己垛膝,他們只是感情好,可當我...
    茶點故事閱讀 67,785評論 6 392
  • 文/花漫 我一把揭開白布丁稀。 她就那樣靜靜地躺著吼拥,像睡著了一般。 火紅的嫁衣襯著肌膚如雪线衫。 梳的紋絲不亂的頭發(fā)上凿可,一...
    開封第一講書人閱讀 51,624評論 1 305
  • 那天,我揣著相機與錄音授账,去河邊找鬼枯跑。 笑死,一個胖子當著我的面吹牛白热,可吹牛的內容都是我干的敛助。 我是一名探鬼主播,決...
    沈念sama閱讀 40,358評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼屋确,長吁一口氣:“原來是場噩夢啊……” “哼纳击!你這毒婦竟也來了续扔?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 39,261評論 0 276
  • 序言:老撾萬榮一對情侶失蹤评疗,失蹤者是張志新(化名)和其女友劉穎测砂,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體百匆,經(jīng)...
    沈念sama閱讀 45,722評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡砌些,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了加匈。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片存璃。...
    茶點故事閱讀 40,030評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖雕拼,靈堂內的尸體忽然破棺而出纵东,到底是詐尸還是另有隱情,我是刑警寧澤啥寇,帶...
    沈念sama閱讀 35,737評論 5 346
  • 正文 年R本政府宣布偎球,位于F島的核電站,受9級特大地震影響辑甜,放射性物質發(fā)生泄漏衰絮。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,360評論 3 330
  • 文/蒙蒙 一磷醋、第九天 我趴在偏房一處隱蔽的房頂上張望猫牡。 院中可真熱鬧,春花似錦邓线、人聲如沸淌友。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,941評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽震庭。三九已至,卻和暖如春你雌,著一層夾襖步出監(jiān)牢的瞬間归薛,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,057評論 1 270
  • 我被黑心中介騙來泰國打工匪蝙, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留主籍,地道東北人。 一個月前我還...
    沈念sama閱讀 48,237評論 3 371
  • 正文 我出身青樓逛球,卻偏偏與公主長得像千元,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子颤绕,可洞房花燭夜當晚...
    茶點故事閱讀 44,976評論 2 355