JavaScript中經常使用的設計模式

本文已同步到Github JavaScript中常見的設計模式,若是感受寫的還能夠,就給個小星星吧,歡迎star和收藏。html

最近拜讀了曾探大神的《JavaScript設計模式與開發實踐》,真是醍醐灌頂,猶如打通任督二脈的感受,讓我對JavaScript的理解加深了不少。git

本文中關於各類設計模式定義都是引用書中的,部分引用自百度百科已標出。另外,本文中所舉例子大可能是書中的,自已作了一些修改和補充,用ES6(書中都是ES5的方式)的方式實現,以加深本身對「類」的理解,並非本身來說解設計模式,主要是作一些筆記以方便本身事後複習與加深理解,同時也但願把書中典型的例子整理出來和你們分享,共同探討和進步。es6

一提起設計模式,相信你們都會脫口而出,23種設計模式,五大設計原則。這裏就不說了,奈何我功力遠遠不夠啊。下面把我整理出的經常使用JavaScript設計模式按類型作個表格整理。本文較長,若是閱讀起來不方便,可連接到個人github中,單獨查看每一種設計模式。先整理這些,後續會繼續補充,感興趣的同窗能夠關注。github

模式分類 名稱
建立型 工廠模式
單例模式
原型模式
結構型 適配器模式
代理模式
行爲型 策略模式
迭代器模式
觀察者模式(發佈-訂閱模式)
命令模式
狀態模式

建立型模式

工廠模式

工廠模式中,咱們在建立對象時不會對客戶端暴露建立邏輯,而且是經過使用一個共同的接口來指向新建立的對象,用工廠方法代替new操做的一種模式。
class Creator {
    create(name) {
        return new Animal(name)
    }
}

class Animal {
    constructor(name) {
        this.name = name
    }
}

var creator = new Creator()

var duck = creator.create('Duck')
console.log(duck.name) // Duck

var chicken = creator.create('Chicken') 
console.log(chicken.name) // Chicken

小結:算法

  1. 構造函數和建立者分離,對new操做進行封裝
  2. 符合開放封閉原則

單例模式

設計模式-單例模式

舉一個書中登陸框的例子,代碼以下:segmentfault

<!DOCTYPE html>
<html lang="en">

<body>
    <button id="btn">登陸</button>
</body>
<script>
    class Login {
        createLayout() {
            var oDiv = document.createElement('div')
            oDiv.innerHTML = '我是登陸框'
            document.body.appendChild(oDiv)
            oDiv.style.display = 'none'
            return oDiv
        }
    }

    class Single {
        getSingle(fn) {
            var result;
            return function() {
                return result || (result = fn.apply(this, arguments))
            }
        }
    }

    var oBtn = document.getElementById('btn')
    var single = new Single()
    var login = new Login()

    // 因爲閉包,createLoginLayer對result的引用,因此當single.getSingle函數執行完以後,內存中並不會銷燬result。

    // 當第二次之後點擊按鈕,根據createLoginLayer函數的做用域鏈中已經包含了result,因此直接返回result

    // 講獲取單例和建立登陸框的方法解耦,符合開放封閉原則
    var createLoginLayer = single.getSingle(login.createLayout)
    oBtn.onclick = function() {
        var layout = createLoginLayer()
        layout.style.display = 'block'
    }
</script>

</html>

小結:設計模式

1.單例模式的主要思想就是,實例若是已經建立,則直接返回數組

function creatSingleton() {
    var obj = null
    // 實例如已經建立過,直接返回
    if (!obj) {
        obj = xxx
    }
    return obj
}

2.符合開放封閉原則瀏覽器

原型模式

用原型實例指定建立對象的種類,而且經過拷貝這些原型建立新的對象。-- 百度百科

在JavaScript中,實現原型模式是在ECMAScript5中,提出的Object.create方法,使用現有的對象來提供新建立的對象的__proto__。緩存

var prototype = {
    name: 'Jack',
    getName: function() {
        return this.name
    }
}

var obj = Object.create(prototype, {
    job: {
        value: 'IT'
    }
})

console.log(obj.getName())  // Jack
console.log(obj.job)  // IT
console.log(obj.__proto__ === prototype)  //true

更多關於prototype的知識能夠看我以前的JavaScript中的面向對象、原型、原型鏈、繼承,下面列一下關於prototype的一些使用方法

1. 方法繼承

var Parent = function() {}
Parent.prototype.show = function() {}
var Child = function() {}

// Child繼承Parent的全部原型方法
Child.prototype = new Parent()

2. 全部函數默認繼承Object

var Foo = function() {}
console.log(Foo.prototype.__proto__ === Object.prototype) // true

3. Object.create

var proto = {a: 1}
var propertiesObject = {
    b: {
        value: 2
    }
}
var obj = Object.create(proto, propertiesObject)
console.log(obj.__proto__ === proto)  // true

4. isPrototypeOf

prototypeObj是否在obj的原型鏈上

prototypeObj.isPrototypeOf(obj)

5. instanceof

contructor.prototype是否出如今obj的原型鏈上

obj instanceof contructor

6. getPrototypeOf

Object.getPrototypeOf(obj) 方法返回指定對象obj的原型(內部[[Prototype]]屬性的值)

Object.getPrototypeOf(obj)

7. setPrototypeOf

設置一個指定的對象的原型 ( 即, 內部[[Prototype]]屬性)到另外一個對象或 null

var obj = {}
var prototypeObj = {}
Object.setPrototypeOf(obj, prototypeObj)
console.log(obj.__proto__ === prototypeObj)  // true

結構型模式

適配器模式

設計模式-適配器模式

舉一個書中渲染地圖的例子

class GooleMap {
    show() {
        console.log('渲染谷歌地圖')
    }
}

class BaiduMap {
    show() {
        console.log('渲染百度地圖')
    }
}

function render(map) {
    if (map.show instanceof Function) {
        map.show()
    }
}

render(new GooleMap())  // 渲染谷歌地圖
render(new BaiduMap())  // 渲染百度地圖

可是假如BaiduMap類的原型方法不叫show,而是叫display,這時候就可使用適配器模式了,由於咱們不能輕易的改變第三方的內容。在BaiduMap的基礎上封裝一層,對外暴露show方法。

class GooleMap {
    show() {
        console.log('渲染谷歌地圖')
    }
}

class BaiduMap {
    display() {
        console.log('渲染百度地圖')
    }
}


// 定義適配器類, 對BaiduMap類進行封裝
class BaiduMapAdapter {
    show() {
        var baiduMap = new BaiduMap()
        return baiduMap.display() 
    }
}

function render(map) {
    if (map.show instanceof Function) {
        map.show()
    }
}

render(new GooleMap())         // 渲染谷歌地圖
render(new BaiduMapAdapter())  // 渲染百度地圖

小結:

  1. 適配器模式主要解決兩個接口之間不匹配的問題,不會改變原有的接口,而是由一個對象對另外一個對象的包裝。
  2. 適配器模式符合開放封閉原則

代理模式

設計模式-代理模式

本文舉一個使用代理對象加載圖片的例子來理解代理模式,當網絡很差的時候,圖片的加載須要一段時間,這就會產生空白,影響用戶體驗,這時候咱們可在圖片真正加載完以前,使用一張loading佔位圖片,等圖片真正加載完再給圖片設置src屬性。

class MyImage {
    constructor() {
        this.img = new Image()
        document.body.appendChild(this.img)
    }
    setSrc(src) {
        this.img.src = src
    }
}

class ProxyImage {
    constructor() {
        this.proxyImage = new Image()
    }

    setSrc(src) {
        let myImageObj = new MyImage()
        myImageObj.img.src = 'file://xxx.png'  //爲本地圖片url
        this.proxyImage.src = src
        this.proxyImage.onload = function() {
            myImageObj.img.src = src
        }
    }
}

var proxyImage = new ProxyImage()
proxyImage.setSrc('http://xxx.png') //服務器資源url

本例中,本體類中有本身的setSrc方法,若是有一天網絡速度已經不須要預加載了,咱們能夠直接使用本體對象的setSrc方法,,而且不須要改動本體類的代碼,並且能夠刪除代理類。

// 依舊能夠知足需求
var myImage = new MyImage()
myImage.setSrc('http://qiniu.sunzhaoye.com/CORS.png')

小結:

  1. 代理模式符合開放封閉原則
  2. 本體對象和代理對象擁有相同的方法,在用戶看來並不知道請求的本體對象仍是代理對象。

行爲型模式

策略模式

定義一系列的算法,把它們一個個封裝起來,並使它們能夠替換
var fnA = function(val) {
    return val * 1
}

var fnB = function(val) {
    return val * 2
}

var fnC = function (val) {
    return val * 3
}


var calculate = function(fn, val) {
    return fn(val)
}

console.log(calculate(fnA, 100))// 100
console.log(calculate(fnB, 100))// 200
console.log(calculate(fnC, 100))// 300

迭代器模式

設計模式-迭代器模式

直接上代碼, 實現一個簡單的迭代器

class Creater {
    constructor(list) {
        this.list = list
    }

    // 建立一個迭代器,也叫遍歷器
    createIterator() {
        return new Iterator(this)
    }
}

class Iterator {
    constructor(creater) {
        this.list = creater.list
        this.index = 0
    }

    // 判斷是否遍歷完數據
    isDone() {
        if (this.index >= this.list.length) {
            return true
        }
        return false
    }

    next() {
        return this.list[this.index++]
    }

}

var arr = [1, 2, 3, 4]

var creater = new Creater(arr)
var iterator = creater.createIterator()
console.log(iterator.list)  // [1, 2, 3, 4]
while (!iterator.isDone()) {
    console.log(iterator.next()) 
    // 1
    // 2
    // 3
    // 4
}

ES6中的迭代器:

JavaScript中的有序數據集合包括:

  • Array
  • Map
  • Set
  • String
  • typeArray
  • arguments
  • NodeList

注意: Object不是有序數據集合

以上有序數據集合都部署了Symbol.iterator屬性,屬性值爲一個函數,執行這個函數,返回一個迭代器,迭代器部署了next方法,調用迭代器的next方法能夠按順序訪問子元素

以數組爲例測試一下,在瀏覽器控制檯中打印測試以下:
Symbol-iterator

var arr = [1, 2, 3, 4]

var iterator = arr[Symbol.iterator]()

console.log(iterator.next())  // {value: 1, done: false}
console.log(iterator.next())  // {value: 2, done: false}
console.log(iterator.next())  // {value: 3, done: false}
console.log(iterator.next())  // {value: 4, done: false}
console.log(iterator.next())  // {value: undefined, done: true}

小結:

  1. JavaScript中的有序數據集合有Array,Map,Set,String,typeArray,arguments,NodeList,不包括Object
  2. 任何部署了[Symbol.iterator]接口的數據均可以使用for...of循環遍歷
  3. 迭代器模式使目標對象和迭代器對象分離,符合開放封閉原則

觀察者模式(訂閱-發佈模式)

設計模式-觀察者模式

先實現一個簡單的發佈-訂閱模式,代碼以下:

class Event {
    constructor() {
        this.eventTypeObj = {}
    }
    on(eventType, fn) {
        if (!this.eventTypeObj[eventType]) {
            // 按照不一樣的訂閱事件類型,存儲不一樣的訂閱回調
            this.eventTypeObj[eventType] = []
        }
        this.eventTypeObj[eventType].push(fn)
    }
    emit() {
        // 能夠理解爲arguments借用shift方法
        var eventType = Array.prototype.shift.call(arguments)
        var eventList = this.eventTypeObj[eventType]
        for (var i = 0; i < eventList.length; i++) {
            eventList[i].apply(eventList[i], arguments)
        }
    }
    remove(eventType, fn) {
        // 若是使用remove方法,fn爲函數名稱,不能是匿名函數
        var eventTypeList = this.eventTypeObj[eventType]
        if (!eventTypeList) {
            // 若是沒有被人訂閱改事件,直接返回
            return false
        }
        if (!fn) {
            // 若是沒有傳入取消訂閱的回調函數,則改訂閱類型的事件所有取消
            eventTypeList && (eventTypeList.length = 0)
        } else {
            for (var i = 0; i < eventTypeList.length; i++) {
                if (eventTypeList[i] === fn) {
                    eventTypeList.splice(i, 1)
                    // 刪除以後,i--保證下輪循環不會漏掉沒有被遍歷到的函數名
                    i--;
                }
            }
        }
    }
}
var handleFn = function(data) {
    console.log(data)
}
var event = new Event()
event.on('click', handleFn)
event.emit('click', '1')   // 1
event.remove('click', handleFn)
event.emit('click', '2')  // 不打印

以上代碼能夠知足先訂閱後發佈,可是若是先發布消息,後訂閱就不知足了。這時候咱們能夠稍微修改一下便可知足先發布後訂閱,在發佈消息時,把事件緩存起來,等有訂閱者時再執行。代碼以下:

class Event {
    constructor() {
        this.eventTypeObj = {}
        this.cacheObj = {}
    }
    on(eventType, fn) {
        if (!this.eventTypeObj[eventType]) {
            // 按照不一樣的訂閱事件類型,存儲不一樣的訂閱回調
            this.eventTypeObj[eventType] = []
        }
        this.eventTypeObj[eventType].push(fn)

        // 若是是先發布,則在訂閱者訂閱後,則根據發佈後緩存的事件類型和參數,執行訂閱者的回調
        if (this.cacheObj[eventType]) {
            var cacheList = this.cacheObj[eventType]
            for (var i = 0; i < cacheList.length; i++) {
                cacheList[i]()
            }
        }
    }
    emit() {
        // 能夠理解爲arguments借用shift方法
        var eventType = Array.prototype.shift.call(arguments)
        var args = arguments
        var that = this

        function cache() {
            if (that.eventTypeObj[eventType]) {
                var eventList = that.eventTypeObj[eventType]
                for (var i = 0; i < eventList.length; i++) {
                    eventList[i].apply(eventList[i], args)
                }
            }
        }
        if (!this.cacheObj[eventType]) {
            this.cacheObj[eventType] = []
        }

        // 若是先訂閱,則直接訂閱後發佈
        cache(args)

        // 若是先發布後訂閱,則把發佈的事件類型與參數保存起來,等到有訂閱後執行訂閱
        this.cacheObj[eventType].push(cache)
    }
}

小結:

  1. 發佈訂閱模式可使代碼解耦,知足開放封閉原則
  2. 當過多的使用發佈訂閱模式,若是訂閱消息始終都沒有觸發,則訂閱者一直保存在內存中。

命令模式

設計模式-命令模式 --百度百科

在命令的發佈者和接收者之間,定義一個命令對象,命令對象暴露出一個統一的接口給命令的發佈者,而命令的發佈者不用去管接收者是如何執行命令的,作到命令發佈者和接收者的解耦。

舉一個若是頁面中有3個按鈕,給不一樣按鈕添加不一樣功能的例子,代碼以下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>cmd-demo</title>
</head>
<body>
    <div>
        <button id="btn1">按鈕1</button>
        <button id="btn2">按鈕2</button>
        <button id="btn3">按鈕3</button>
    </div>
    <script>
        var btn1 = document.getElementById('btn1')
        var btn2 = document.getElementById('btn2')
        var btn3 = document.getElementById('btn3')

        // 定義一個命令發佈者(執行者)的類
        class Executor {
            setCommand(btn, command) {
                btn.onclick = function() {
                    command.execute()
                }
            }
        }

        // 定義一個命令接收者
        class Menu {
            refresh() {
                console.log('刷新菜單')
            }

            addSubMenu() {
                console.log('增長子菜單')
            }
        }

        // 定義一個刷新菜單的命令對象的類
        class RefreshMenu {
            constructor(receiver) {
                // 命令對象與接收者關聯
                this.receiver = receiver
            }

            // 暴露出統一的接口給命令發佈者Executor
            execute() {
                this.receiver.refresh()
            }
        }

        // 定義一個增長子菜單的命令對象的類
        class AddSubMenu {
            constructor(receiver) {
                // 命令對象與接收者關聯
                this.receiver = receiver
            }
            // 暴露出統一的接口給命令發佈者Executor
            execute() {
                this.receiver.addSubMenu()
            }
        }

        var menu = new Menu()
        var executor = new Executor()

        var refreshMenu = new RefreshMenu(menu)
        // 給按鈕1添加刷新功能
        executor.setCommand(btn1, refreshMenu)

        var addSubMenu = new AddSubMenu(menu)
        // 給按鈕2添加增長子菜單功能
        executor.setCommand(btn2, addSubMenu)

        // 若是想給按鈕3增長刪除菜單的功能,就繼續增長刪除菜單的命令對象和接收者的具體刪除方法,而沒必要修改命令對象
    </script>
</body>
</html>

狀態模式

設計模式-狀態模式

舉一個關於開關控制電燈的例子,電燈只有一個開關,第一次按下打開弱光,第二次按下打開強光,第三次按下關閉。

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <title>state-demo</title>
</head>

<body>
    <button id="btn">開關</button>
    <script>
        // 定義一個關閉狀態的類   
        class OffLightState {
            constructor(light) {
                this.light = light
            }
            // 每一個類都須要這個方法,在不一樣狀態下按都須要觸發這個方法
            pressBtn() {
                this.light.setState(this.light.weekLightState)
                console.log('開啓弱光')
            }
        }

        // 定義一個弱光狀態的類   
        class WeekLightState {
            constructor(light) {
                this.light = light
            }
            pressBtn() {
                this.light.setState(this.light.strongLightState)
                console.log('開啓強光')
            }
        }

        // 定義一個強光狀態的類
        class StrongLightState {
            constructor(light) {
                this.light = light
            }
            pressBtn() {
                this.light.setState(this.light.offLightState)
                console.log('關閉電燈')
            }
        }

        class Light {
            constructor() {
                this.offLightState = new OffLightState(this)
                this.weekLightState = new WeekLightState(this)
                this.strongLightState = new StrongLightState(this)
                this.currentState = null
            }
            setState(newState) {
                this.currentState = newState
            }
            init() {
                this.currentState = this.offLightState
            }
        }

        let light = new Light()
        light.init()
        var btn = document.getElementById('btn')
        btn.onclick = function() {
            light.currentState.pressBtn()
        }
    </script>
</body>

</html>

若是這時候須要增長一個超強光,則只需增長一個超強光的類,並添加pressBtn方法,改變強光狀態下,點擊開關須要把狀態更改成超強光,超強光狀態下,點擊開關把狀態改成關閉便可,其餘代碼都不須要改動。

class StrongLightState {
    constructor(light) {
        this.light = light
    }
    pressBtn() {
        this.light.setState(this.light.superLightState)
        console.log('開啓超強光')
    }
}

class SuperLightState {
    constructor(light) {
        this.light = light
    }
    pressBtn() {
        this.light.setState(this.light.offLightState)
        console.log('關閉電燈')
    }
}

class Light {
    constructor() {
        this.offLightState = new OffLightState(this)
        this.weekLightState = new WeekLightState(this)
        this.strongLightState = new StrongLightState(this)
        this.superLightState = new SuperLightState(this)
        this.currentState = null
    }
    setState(newState) {
        this.currentState = newState
    }
    init() {
        this.currentState = this.offLightState
    }
}

小結:

  1. 經過定義不一樣的狀態類,根據狀態的改變而改變對象的行爲,二沒必要把大量的邏輯都寫在被操做對象的類中,並且容易增長新的狀態
  2. 符合開放封閉原則

終於到最後可,歷時多日地閱讀與理解,並記錄與整理筆記,目前整理出10中JavaScript中常見的設計模式,後續會對筆記繼續整理,而後加以補充。因爲筆者功力比較淺,若有問題,還望你們多多指正,謝謝。

參考文章:

JavaScript設計模式與開發實踐
深刻理解JavaScript系列/設計模式--湯姆大叔的博客
設計模式--菜鳥教程
JavaScript 中常見設計模式整理
ES6入門--阮一峯

相關文章
相關標籤/搜索