JavaScript設計模式總結

jefferyE發表於2019-03-25

之前看過《JavaScript設計模式與開發實踐》這本書,對書中的設計模式和一些相關案例也有了一定的瞭解,同時把這些設計模式的應用對應在在一些其他的專案中,進行了一些整理,如下僅供參考:

補充:如果以下內容有什麼不對的地方,歡迎指正。

設計模式目的

設計模式是為了更好的程式碼重用性,可讀性,可靠性,可維護性。

設計六大原則

1)單一職責原則
2)里氏替換原則
3)依賴倒轉原則
4)介面隔離原則
5)最少知識原則(迪米特法則)
6)開放封閉原則

設計模式分類

總體來說設計模式分為三大類:

建立型模式,共五種:工廠方法模式、抽象工廠模式、單例模式、建造者模式、原型模式。

結構型模式,共七種:介面卡模式、裝飾器模式、代理模式、外觀模式、橋接模式、組合模式、享元模式。

行為型模式,共十一種:策略模式、模板方法模式、觀察者模式、迭代子模式、責任鏈模式、命令模式、備忘錄模式、狀態模式、訪問者模式、中介者模式、直譯器模式。

其實還有兩類:併發型模式和執行緒池模式。

不過,對於前端來說,有的設計模式在平時工作中幾乎用不到或者很少用到,來來來,來了解下前端常見的設計模式

JS中的設計模式

常見設計模式:

1、工廠模式

常見的例項化物件模式,工廠模式就相當於建立例項物件的new,提供一個建立物件的介面

    // 某個需要建立的具體物件
    class Product {
        constructor (name) {
            this.name = name;
        }
        init () {}
    }
    // 工廠物件
    class Creator {
        create (name) {
            return new Product(name);
        }
    }
    const creator = new Creator();
    const p = creator.create(); // 通過工廠物件建立出來的具體物件
複製程式碼

應用場景:JQuery中的$、Vue.component非同步元件、React.createElement等

2、單例模式

保證一個類僅有一個例項,並提供一個訪問它的全域性訪問點,一般登入、購物車等都是一個單例。

    // 單例物件
    class SingleObject {
        login () {}
    }
    // 訪問方法
    SingleObject.getInstance = (function () {
        let instance;
        return function () {
            if (!instance) {
                instance = new SingleObject();
            }
            return instance;
        }
    })()
    const obj1 = SingleObject.getInstance();
    const obj2 = SingleObject.getInstance();
    console.log(obj1 === obj2); // true
複製程式碼

應用場景:JQuery中的$、Vuex中的Store、Redux中的Store等

3、介面卡模式

用來解決兩個介面不相容問題,由一個物件來包裝不相容的物件,比如引數轉換,允許直接訪問

    class Adapter {
        specificRequest () {
            return '德國標準插頭';
        }
    }
    // 介面卡物件,對原來不相容物件進行包裝處理
    class Target {
        constructor () {
            this.adapter = new Adapter();
        }
        request () {
            const info = this.adapter.specificRequest();
            console.log(`${info} - 轉換器 - 中國標準插頭`)
        }
    }
    const target = new Target();
    console.log(target.request()); // 德國標準插頭 - 轉換器 - 中國標準插頭
複製程式碼

應用場景:Vue的computed、舊的JSON格式轉換成新的格式等

4、裝飾器模式

在不改變物件自身的基礎上,動態的給某個物件新增新的功能,同時又不改變其介面

    class Plane {
        fire () {
            console.log('傳送普通子彈');
        }
    }
    // 裝飾過的物件
    class Missile {
        constructor (plane) {
            this.plane = plane;
        }
        fire () {
            this.plane.fire();
            console.log('發射導彈');
        }
    }
    let plane = new Plane();
    plane = new Missile(plane);
    console.log(plane.fire()); // 依次列印 傳送普通子彈 發射導彈
複製程式碼

利用AOP給函式動態新增功能,即Function的after或者before

Function.prototype.before = function (beforeFn) {
  const _self = this;
  return function () {
    beforeFn.apply(this, arguments);
    return _self.apply(this, arguments);
  }
}

Function.prototype.after = function (afterFn) {
  const _self = this;
  return function () {
    const ret = _self.apply(this, arguments);
    afterFn.apply(this, arguments);
    return ret;
  }
}

let func = function () {
  console.log('2');
}

func = func.before(function() {
  console.log('1');
}).after(function() {
  console.log('3');
})

func();
console.log(func()); // 依次列印 1 2 3
複製程式碼

應用場景:ES7裝飾器、Vuex中1.0版本混入Vue時,重寫init方法、Vue中陣列變異方法實現等

5、代理模式

為其他物件提供一種代理,便以控制對這個物件的訪問,不能直接訪問目標物件

class Flower {}
// 源物件
class Jack {
    constructor (target) {
        this.target = target;
    }
    sendFlower (target) {
        const flower = new Flower();
        this.target.receiveFlower(flower)
    }
}
// 目標物件
class Rose {
    receiveFlower (flower) {
        console.log('收到花: ' + flower)
    }
}
// 代理物件
class ProxyObj {
    constructor () {
        this.target = new Rose();
    }
    receiveFlower (flower) {
        this.sendFlower(flower)
    }
    sendFlower (flower) {
        this.target.receiveFlower(flower)
    }
}
const proxyObj = new ProxyObj();
const jack = new Jack(proxyObj);
jack.sendFlower(proxyObj); // 收到花:[object Object]
複製程式碼

應用場景:ES6 Proxy、Vuex中對於getters訪問、圖片預載入等

6、外觀模式

為一組複雜的子系統介面提供一個更高階的統一介面,通過這個介面使得對子系統介面的訪問更容易,不符合單一職責原則和開放封閉原則

 class A {
    eat () {}
}
class  B {
    eat () {}
}
class C {
    eat () {
        const a = new A();
        const b = new B();
        a.eat();
        b.eat();
    }
}
// 跨瀏覽器事件偵聽器
function addEvent(el, type, fn) {
    if (window.addEventListener) {
        el.addEventListener(type, fn, false);
    } else if (window.attachEvent) {
        el.attachEvent('on' + type, fn);
    } else {
        el['on' + type] = fn;
    }
}
複製程式碼

應用場景:JS事件不同瀏覽器相容處理、同一方法可以傳入不同引數相容處理等

7、觀察者模式

定義物件間的一種一對多的依賴關係,當一個物件的狀態發生改變時,所有依賴於它的物件都將得到通知

   class Subject {
  constructor () {
    this.state = 0;
    this.observers = [];
  }
  getState () {
    return this.state;
  }
  setState (state) {
    this.state = state;
    this.notify();
  }
  notify () {
    this.observers.forEach(observer => {
      observer.update();
    })
  }
  attach (observer) {
    this.observers.push(observer);
  }
}


class Observer {
  constructor (name, subject) {
    this.name = name;
    this.subject = subject;
    this.subject.attach(this);
  }
  update () {
    console.log(`${this.name} update, state: ${this.subject.getState()}`);
  }
}

let sub = new Subject();
let observer1 = new Observer('o1', sub);
let observer2 = new Observer('o2', sub);

sub.setState(1);
複製程式碼

觀察者模式與釋出/訂閱模式區別: 本質上的區別是排程的地方不同

雖然兩種模式都存在訂閱者和釋出者(具體觀察者可認為是訂閱者、具體目標可認為是釋出者),但是觀察者模式是由具體目標排程的,而釋出/訂閱模式是統一由排程中心調的,所以觀察者模式的訂閱者與釋出者之間是存在依賴的,而釋出/訂閱模式則不會。

---觀察者模式:目標和觀察者是基類,目標提供維護觀察者的一系列方法,觀察者提供更新介面。具體觀察者和具體目標繼承各自的基類,然後具體觀察者把自己註冊到具體目標裡,在具體目標發生變化時候,排程觀察者的更新方法。
比如有個“天氣中心”的具體目標A,專門監聽天氣變化,而有個顯示天氣的介面的觀察者B,B就把自己註冊到A裡,當A觸發天氣變化,就排程B的更新方法,並帶上自己的上下文。

---釋出/訂閱模式:訂閱者把自己想訂閱的事件註冊到排程中心,當該事件觸發時候,釋出者釋出該事件到排程中心(順帶上下文),由排程中心統一排程訂閱者註冊到排程中心的處理程式碼。
比如有個介面是實時顯示天氣,它就訂閱天氣事件(註冊到排程中心,包括處理程式),當天氣變化時(定時獲取資料),就作為釋出者釋出天氣資訊到排程中心,排程中心就排程訂閱者的天氣處理程式。

應用場景:JS事件、JS Promise、JQuery.$CallBack、Vue watch、NodeJS自定義事件,檔案流等

8、迭代器模式

提供一種方法順序訪問一個聚合物件中各個元素, 而又無須暴露該物件的內部表示

可分為:內部迭代器和外部迭代器

內部迭代器: 內部已經定義好迭代規則,外部只需要呼叫一次即可。

const each = (args, fn) => {
  for (let i = 0, len = args.length; i < len; i++) {
    const value = fn(args[i], i, args);

    if (value === false) break;
  }
}
複製程式碼

應用場景: JQuery.each方法

外部迭代器:必須顯示的請求迭代下一個元素。

// 迭代器
class Iterator {
  constructor (list) {
    this.list = list;
    this.index = 0;
  }
  next () {
    if (this.hasNext()) {
      return this.list[this.index++]
    }
    return null;
  }
  hasNext () {
    if (this.index === this.list.length) {
      return false;
    }
    return true;
  }
}
const arr = [1, 2, 3, 4, 5, 6];
const ite = new Iterator();

while(ite.hasNext()) {
  console.log(ite.next()); // 依次列印 1 2 3 4 5 6
}
複製程式碼

應用場景:JS Iterator、JS Generator

9、狀態模式

關鍵是區分事物內部的狀態,事物內部狀態往往會帶來事物的行為改變,即允許物件在內部狀態發生改變時改變它的行為

// 紅燈
class RedLight {
    constructor (state) {
        this.state = state;
    }
    light () {
        console.log('turn to red light');
        this.state.setState(this.state.greenLight)
    }
}
// 綠燈
class greenLight {
    constructor (state) {
        this.state = state;
    }
    light () {
        console.log('turn to green light');
        this.state.setState(this.state.yellowLight)
    }
}
// 黃燈
class yellowLight {
    constructor (state) {
        this.state = state;
    }
    light () {
        console.log('turn to yellow light');
        this.state.setState(this.state.redLight)
    }
}
class State {
    constructor () {
        this.redLight = new RedLight(this)
        this.greenLight = new greenLight(this)
        this.yellowLight = new yellowLight(this)
        this.setState(this.redLight) // 初始化為紅燈
    }
    setState (state) {
        this.currState = state;
    }
}
const state = new State();
state.currState.light() // turn to red light
setInterval(() => {
    state.currState.light() // 每隔3秒依次列印紅燈、綠燈、黃燈
}, 3000)
複製程式碼

應用場景:燈泡狀態、紅綠燈切換等

其他設計模式:

10、命令模式
11、組合模式
12、享元模式
13、策略模式
14、職責鏈模式
15、模板方法模式
16、中介者模式
17、備忘錄模式
18、訪問者模式
19、直譯器模式
20、橋接模式

其他設計模式請移步:github.com/jefferyE
更多設計模式,具體請參考:www.runoob.com

相關文章