JS 设计模式

设计模式是什么

  • 设计模式就是前人总结出来的一套更加容易阅读、维护以及复用的写代码的方式
  • 模式就是一个我们如何解决问题的模版

  • 很多用起来顺手的开源库都有良好的封装,封装可以将内部环境和外部环境隔离,外部用起来更顺手。
  • 针对不同的场景可以有不同的封装方案。
    • 需要大量产生类似实例的组件可以考虑用工厂模式来封装。
    • 内部逻辑较复杂,外部使用时需要的实例也不多,可以考虑用建造者模式来封装。
    • 全局只能有一个实例的需要用单例模式来封装。
    • 新老对象之间可能有继承关系的可以考虑用原型模式来封装,JS本身就是一个典型的原型模式。
  • 使用设计模式时不要生搬硬套代码模板,更重要的是掌握思想,同一个模式在不同的场景可以有不同的实现方案。

  • 用于代码封装的设计模式主要有四种:工厂模式、单例模式、原型模式、创建者模式

封装的好处

  • 内部变量不会污染外部
  • 可以作为一个模块给外部调用
  • 对扩展开放,对修改关闭,即开闭原则。外部不能修改模块,既保证了模块内部的正确性,又可以留出扩展接口,使用灵活。

工厂模式

  • 简单工厂模式就是由一个方法来决定到底该创建哪个类的实例,这些实例通常有相同的接口 ( 属性和方法 )
  • 优点:你只需要一个正确的参数就能获取到你想要的对象
  • 实例:提示弹窗的种类,jQuery 也是一个典型的工厂模式,你给他一个参数,他就给你返回符合参数 DOM 对象
function infoPopup(content, color) {}
function confirmPopup(content, color) {}
function cancelPopup(content, color) {}
// 新加一个方法popup把这几个类都包装起来
function popup(type, content, color) {
    switch (type) {
        case 'infoPopup':
            return new infoPopup(content, color);
        case 'confirmPopup':
            return new confirmPopup(content, color);
        case 'cancelPopup':
            return new cancelPopup(content, color);
    }
}
function popup(type, content, color) {
    // 如果是通过new调用的,返回对应类型的弹窗
    if (this instanceof popup) {
        return new this[type](content, color);
    } else {
        // 如果不是new调用的,使用new调用,会走到上面那行代码
        return new popup(type, content, color);
    }
}
// 各种类型的弹窗全部挂载在原型上成为实例方法
popup.prototype.infoPopup = function (content, color) {}
popup.prototype.confirmPopup = function (content, color) {}
popup.prototype.cancelPopup = function (content, color) {}
单例模式 Singleton Pattern
  • 单例就是保证一个类只有一个实例,实现的方法一般是先判断实例存在与否,如果存在直接返回,如果不存在就创建了再返回,这就确保了一个类只有一个实例对象。
  • 把描述同一件事物的属性和特征进行 分组、归类 ( 存储在同一个堆内存空间中 ),因此避免了全局变量之间的冲突和污染
  • 在单例模式中 object 不仅仅是对象名,它被称为命名空间 [ NameSpace ],把描述事物的属性放到命名空间中,多个命名空间是独立分开的互不冲突。
// 高级单例模式
const pattern1 = (function() {
    let n = 'xxx'
    function fn() {
        //...
    }
    return {
        fn
    }
})()
// 一个自定义函数执行形成一个私有作用域
// 函数返回一个使用这个作用域中变量、属性的对象并被外面的变量占用
// 此时函数形成的私有作用域不会被销毁,即形成一个不销毁的栈内存
  • 再给命名空间赋值的时候不是直接赋值一个对象,而是先执行匿名函数形成一个私有作用域 A(不销毁的栈内存),在 A 中创建一个堆内存并把堆内存的地址赋值给命名空间

  • 限制一个类的实例化只能一次,一个类只有一个实例,并且提供一个访问它的全局访问点。

  • 单例模式是创建型模式的一种。针对全局仅需一个对象的场景,比如全局缓存、全局状态管理、window 对象、线程池等等这些

class SingleTon {
    constructor(name) {
        this.name = name
        this.instance = null
    }
}

SingleTon.getInstance = function(name) {
    if(!this.instance) {
        this.instance = new SingleTon(name)
    }
    return this.instance
}

let s1 = SingleTon.getInstance('s1')
let s2 = SingleTon.getInstance('s2')
console.log(s1 === s2) // true
console.log(s1.name) // s1
console.log(s2.name) // s1
构造函数模式
  • 主要用于组件、类库、插件、组件的封装
适配器模式

适配器用来解决两个接口不兼容的情况,不需要改变已有的接口,通过包装一层的方式实现两个接口的相互协作。

// 老接口
const oldCity = (function(){
    return [{
        name: 'beijing',
        id: 11
    }, {
        name: 'baoding',
        id: 12
    }]
})()

// 新接口希望是下面形式
// {
//     hangzhou: 11,
//     jinhua: 12
// }
// 这时候就可采用适配者模式
const adaptor = function(oldCity) {
    let obj = {}

    for(let city of oldCity) {
        obj[city.name] = city.id
    }
    return obj
}
console.log(adaptor(oldCity)) // {beijing: 11, baoding: 12}
装饰模式

装饰模式不需要改变已有的接口,作用是给对象添加功能。

var plan = {
    fire: function() {
        console.log('发射子弹')
    }
}
plan.fire() // 发射子弹

var fire1 = plan.fire
var shot = function() {
    console.log('发射散弹')
}
plan.fire = function() {
    fire1()
    shot()
}
plan.fire() // 发射子弹 发射散弹

var fire2 = plan.fire
var track = function() {
    console.log('发射跟踪导弹')
}
plan.fire = function() {
    fire2()
    track()
}
plan.fire() // 发射子弹 发射散弹 发射跟踪导弹
代理模式

代理是为了控制对对象的访问,不让外部直接问访问到对象。
事件委托/代理 就用到了代理模式

示例如下:

  1. 小明送花
var Flower = function(num = '999') {
    this.num = num
    this.getNum = function() {
        return `${this.num}朵`
    }
}
var xiaoming = {
    sendFlower: function(target) {
        var flower = new Flower()
        target.receiveFlower(flower)
    }
}
var xiaohua = {
    receiveFlower: function(flower) {
        console.log(`receive flower ${flower.getNum()}`)
    }
}
xiaoming.sendFlower(xiaohua) // receive flower 999朵
  1. 小明通过小红送花
var Flower = function(num = '999') {
    this.num = num
    this.getNum = function() {
        return `${this.num}朵`
    }
}
var xiaoming = {
    sendFlower: function(target) {
        var flower = new Flower()
        target.receiveFlower(flower)
    }
}
var xiaohong = {
    receiveFlower: function(flower) {
        return xiaohua.receiveFlower(flower)
    }
}
var xiaohua = {
    receiveFlower: function(flower) {
        console.log(`receive flower ${flower.getNum()}`)
    }
}
xiaoming.sendFlower(xiaohong) // receive flower 999朵
  • 很显然,执行结果跟第一段代码一致,至此我们就完成了一个最简单的代理模式的编写。
  • 我们会发现小明自己去送花和代理小红帮小明送花,二者看起来并没有本质的区别,引入一个代理对象看起来只是把事情搞复杂了而已。
  • 的确,此处的代理模式毫无用处,它所做的只是把请求简单地转交给本体。
  • 现在我们改变故事的背景设定,假设当小花在心情好的时候收到花,小明表白成功的几率有60%,而当小花在心情差的时候收到花,小明表白的成功率无限趋近于 0。
  • 小明跟小花刚刚认识两天,还无法辨别她什么时候心情好。如果不合时宜地把花送给她,花被直接扔掉的可能性很大,这束花可是小明吃了 7 天泡面换来的。
  • 但是小花的朋友小红却很了解她,所以小明只管把花交给小红,小红会监听小花的心情变化,然后选择她心情好的时候把花转交给她

代码如下:

var Flower = function(num = '999') {
    this.num = num
    this.getNum = function() {
        return `${this.num}朵`
    }
}
var xiaoming = {
    sendFlower: function(target) {
        var flower = new Flower()
        target.receiveFlower(flower)
    }
}
var xiaohong = {
    receiveFlower: function(flower) {
        xiaohua.listenGoodMood(function() { // 监听 xiaohua 的好心情
            return xiaohua.receiveFlower(flower)
        })
    }
}
var xiaohua = {
    receiveFlower: function(flower) {
        console.log(`receive flower ${flower.getNum()}`)
    },
    listenGoodMood: function(fun) {
        setTimeout(function() { // 假设 10 秒之后 xiaohua 的心情变好
            fun()
        }, 10000)
    }
}
xiaoming.sendFlower(xiaohong) // receive flower 999朵
发布-订阅模式

也叫观察者模式,通过一对一或者一对多的关系,当对象发生改变时订阅双方都会收到通知。
在实际代码中其实发布-订阅模式也很常见,比如我们点击一个按钮触发了点击事件就是使用了该模式

    发布-订阅模式
外观模式

外观模式提供了一个接口,隐藏了内部的逻辑,更加方便外部调用。
举个例子来说,我们现在需要实现一个兼容多种浏览器的添加事件方法

function addEvent(elm, evType, fn, useCapture) {
    if (elm.addEventListener) {
        elm.addEventListener(evType, fn, useCapture)
        return true
    } else if (elm.attachEvent) {
        var r = elm.attachEvent("on" + evType, fn)
        return r
    } else {
        elm["on" + evType] = fn
    }
}

对于不同的浏览器,添加事件的方式可能会存在兼容问题。如果每次都需要去这样写一遍的话肯定是不能接受的,所以我们将这些判断逻辑统一封装在一个接口中,外部需要添加事件只需要调用 addEvent 即可。

你可能感兴趣的:(JS 设计模式)