JavaScript中的设计模式

创建型模式

工厂模式

工厂模式中,我们在创建对象时不会对客户端暴露创建逻辑,并且是通过使用一个共同的接口来指向新创建的对象,用工厂方法代替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. 符合开放封闭原则

单例模式

单例模式的定义是, 保证一个类仅有一个实例, 并提供一个访问他的全局方法。

单例模式是一种常用的模式, 有一些对象我们往往只需要一个, 比如线程池、全局缓存、浏览器中的window对象等。 在JavaScript开发中, 单例模式的用途同样非常广泛。试想一下,当我们单击登录按钮的时候,页面中会出现一个登录浮窗,而这个登录浮窗是唯一的,无论单击多少次登录按钮, 这个浮窗只会被创建一次, name这个登录浮窗就适合单例模式来闯将。

举一个登录框的例子,代码如下:

<!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 createSingleton() {
    let obj = null
    // 实例如果已经创建过,则直接返回
    if (!obj) {
        obj = xxx
    }
    return obj
}
复制代码

2.符合开放封闭原则

结构型模式

适配器模式

适配器模式的作用是解决两个软件实体间的接口不兼容的问题。使用适配器模式后,原来由于接口不兼容而不能工作的两个软件实体可以一起工作。

适配器的别名是包装器(wrapper),这是一个相对简单的模式。在程序开发中有许多这样的场景:当我们试图用模块或对象的某个接口时,却发现这个接口的格式并不符合目前的需求。这时候有两种解决方法,第一种是修改原来的接口实现,但是原来的模块很复杂,或者我们拿到的模块是一段别人编写的经过压缩的代码,修改原接口就显得不太现实了。第二种方法是创建一个适配器,将原接口转换为客户希望的另一个接口,客户只需要和适配器打交道。

举一个渲染地图的例子:

class GoogleMap {
    show() {
        console.info('渲染谷歌地图')
    }
}

class BaiduMap {
    show() {
        console.info('渲染百度地图')
    }
}

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

render(new GoogleMap())  // 渲染谷歌地图
render(new BaiduMap())  // 渲染百度地图
复制代码

但是假如BaiduMap类的原型方法不叫show,而是叫display,这时候就可以使用适配器模式了,因为我们不能轻易的改变第三方的内容。在BaiduMap的基础上封装一层,对外暴露show方法。

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

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


// 定义适配器类, 对BaiduMap类进行封装
class BaiduMapAdapter {
    show() {
        const 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 = 'loading.png'
        this.proxyImage.src = src
        this.proxyImage.onload = function() {
            myImageObj.img.src = src
        }
    }
}

const proxyImage = new ProxyImage()
proxyImage.setSrc('pic.png') 
复制代码

本例中,本体类中有自己的setSrc方法,如果有一天网络速度已经不需要预加载了,我们可以直接使用本体对象的setSrc方法,,并且不需要改动本体类的代码,而且可以删除代理类。

// 依旧可以满足需求
var myImage = new MyImage()
myImage.setSrc('https://images2.alphacoders.com/103/thumbbig-1031920.webp')
复制代码

小结:

  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 Event {
    constructor() {
        this.eventTypeObj = {}
    }
    on(eventType, fn) {
        if (!this.eventTypeObj[eventType]) {
            // 按照不同的订阅事件类型, 储存不同的订阅回调
            this.eventTypeObj[eventType] = []
        }
        this.eventTypeObj[eventType].push(fn)
    }
    emit() {
        // 可以理解为arguments借用shift方法
        let eventType = Array.prototype.shift.call(arguments)
        let eventList = this.eventTypeObj[eventType]
        for (let i = 0; i < eventList.length; i++) {
            eventList[i].apply(eventList[i], arguments)
        }
    }
    remove(eventType, fn) {
        // 如果使用remove方法, fn为函数名称,不能是匿名函数
        let eventTypeList = this.eventTypeObj[eventType]
        if (!eventTypeList) {
            // 如果没有被人订阅过事件, 直接返回
            return false
        }
        if (!fn) {
            // 如果没有传入取消订阅的回调函数,则该订阅类型的事件全部取消
            eventTypeList && (eventTypeList.length = 0)
        } else {
            for (let 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. 当过多的使用发布订阅模式,如果订阅消息始终都没有触发,则订阅者一直保存在内存中。

命令模式

在软件系统中, 行为请求者与行为实现者通常呈现一种紧耦合。但在某些场合, 比如要对行为进行记录、撤销/重做、事务等处理,这种无法抵御变化的紧耦合是不合适的。在这种情况下, 如何将行为请求者与行为实现者解耦?将一组行为抽象为对象,实现二者之间的松耦合。这就是命令模式(Command Pattern)。

在命令的发布者和接收者之间,定义一个命令对象,命令对象暴露出一个统一的接口给命令的发布者,而命令的发布者不用去管接收者是如何执行命令的,做到命令发布者和接收者的解耦。

举一个如果页面中有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.info('刷新菜单')
            }
            addSubMenu() {
                console.info('添加子菜单')
            }
        }

        // 定义一个刷新菜单的命令对象的类
        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>
复制代码
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享