设计模式——JS

130 阅读13分钟

1.工厂模式

由一个工厂对象决定创建某一种产品对象类的实例。主要用来创建同一类对象。

class User(role, pages){
    constructor(role, pages){
        this.role = role
        this.pages = pages
    }
    static UserFactory(role){
        switch(role){
            case "superadmin":
                return new User("superadmin", ["home", "user-manage", "right-manage", "news-manage"])
                break;
            case "admin":
                return new User("admin", ["home", "user-manage", "news-manage"])
            break;
            case "editor":
                return new User("editor", ["home", "news-manage"])
            break;
            default:
                throw new Error("参数错误")

        }
    }
}

var user = User.UserFactory("editor")

简单工厂的优点在于,你只需要一个正确的的参数,就可以获取你所需要的对象,而无需知道其具体创建的具体细节。但是在函数内包含了所有对象的创建逻辑和判断逻辑的代码,每增加新的构造函数还需要修改判断逻辑代码。当我们的对象不是上面的3个而是10个或更多时,这个函数会成为一个庞大的超级函数,变得难以维护。所以,简单工厂只能作用于创建的对象数量较少,对象的创建逻辑不复杂时使用。

2.抽象工厂模式

抽象工厂模式并不直接生成实例,而是用于对产品类的创建

class User {
    constructor(name, role, pages){
        this.name = name
        this.role = role
        this.pages = pages
    }
    welcome(){
        console.log("欢迎回来", this.name)
    }
    dataShow(){
        //抽象
        throw new Error("抽象方法需要被实现")
    }
}

class SuperAdmin extends User{
    constructor(name){
        super(name, "superadmin", ["home", "user-manage", "right-manage", "news-manage"])
    }
    dataShow(){
        console.log("superadmin-datashow")
    }
    addRight(){
    }
    addUser(){
    }
}
class Admin extends User{
    constructor(name){
        super(name, "admin", ["home", "user-manage", "news-manage"])
    }
    dataShow(){
        console.log("admin-datashow")
    }
    addUser(){
    }
}
class Editor extends User{
    constructor(name){
        super(name, "editor", ["home", "news-manage"])
    }
    dataShow(){
        console.log("editor-datashow")
    }
}

function getAbstractUserFactory(role){
    switch(role){
        case "superadmin":
            return SuperAdmin
        case "admin":
            return Admin
        case "editor":
            return Editor
        default:
            throw new ErCror("参数错误")
    }
}

let UserClass = getAbstractUserFactory("editor")
let user = new UserClass("user1")

3.建造者模式

建造者模式属于创建型模式的一种,提供一种创建负责对象的方式。它将一个复杂的对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。

建造者模式是一步一步的创建一个复杂的对象,它允许用户只通过指定复杂的对象的类型和内容就可以构建它们,用户不需要指定内部的具体构造细节。

class NavBar {
    init(){
        console.log("navbar-init")
    }
    getData(){
        console.log("navbar-getData")
    }
    render(){
        console.log("navbar-render")
    }
}
class List {
    init(){
        console.log("list-init")
    }
    getData(){
        console.log("list-getData")
    }
    render(){
        console.log("list-render")
    }
}

class Creator {
    startBuild(builder){
        builder.init()
        builder.getData()
        builder.render()
    }
}
const op = new Creator()
op.startBuild(new NavBar())
op.startBuild(new List())

4.单例模式

1.保证一个类仅有一个实例,并提供一个访问它的全局访问点

2.主要解决一个全局使用的频繁地创建和销毁,占内存

// 利用闭包
var Singleton = (function(){
    var instance
    function User(name, age){
        this.name = name
        this.age = age
    }
    return function(name, age){
        if(!instance){
            //创建实例
            instance = new User(name, age)
        }
        return instance
    }
})()

var instance = Singleton("单例"18)

ES6类的单例:

class Singleton {
    constructor(name, age){
        if(!Singleton.instance){
            this.name = name
            this.age = age
            Singleton.instance = this
        }
        return Singleton.instance
    }
}
new Singleton("单例1", 16) === new Singleton("单例2", 18)

5.装饰器模式

装饰器模式能很好的对已有功能进行扩展,这样不会更改原有代码,对其他的业务产生影响,这方便我们在较小的改动下,对软件功能进行扩展。

ES5:

    Function.prototype.before = function(beforeFn){
        var _this = this
        return function(){
            // 先执行前置函数调用
            beforeFn.apply(this, arguments)
            // 执行原来的函数,并返回原来的执行结果
            return _this.apply(this, arguments)
        }
    }
    
    Function.prototype.after = function(afterFn){
        var _this = this
        return function(){
            // 执行原来的函数,并返回原来的执行结果
            var result = _this.apply(this, arguments)
            // 先执行前置函数调用
            afterFn.apply(this, arguments)
            return result
        }
    }
    
    // test
    function test(){
        console.log("111111")
    }
    
    var test1 = test.before(function(){
        console.log("000000")
    }).after(function(){
        console.log("222222")
    })
    
    test1()

应用:比如用户点击事件的埋点等。可以通过装饰器模式,需要埋点的时候注入,不需要的时候不注入。

6.适配器模式

将一个类的接口转换客户希望的另一个接口。适配器模式让那些接口不兼容的类可以一起工作。

class TencentMap{
    show(){
        console.log("开始渲染腾讯地图")
    }
}

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

// 腾讯地图的适配器
class TencentAdapt extends TencentMap{
    constructor(){
        super()
    }
    // 适配和兼容方法
    display(){
        this.show()
    }
}

function renderMap(map){
    map.display()
}

renderMap(new TencentMap())
renderMap(new BaiduMap())

7.策略模式

策略模式定义了一系列的算法,并将每个算法封装起来,使它们可以互相替换,且算法的变化不会影响使用算法的客户。策略模式属于对象行为模式,它通过对算法进行封装,把使用算法的责任和算法的实现分割开来,并委派给不同的对象对这些算法进行管理。

该模式主要解决在多种算法相似的情况下,使用if...else所带来的复杂和难以维护。它的优点是算法可以自由切换,同时可以避免多重if...else判断,具有良好的可扩展性。

let strategry = {
    'A':(salary) => {
        return salary * 4
    },
    'B':(salary) => {
        return salary * 3
    },
    'C':(salary) => {
        return salary * 2
    }
}

function calBonus(level, salary){
    return strategry[level](salary)
}

8.代理模式

为其他对象提供一种代理以控制对这个对象的访问。代理模式使得代理对象控制具体对象的引用。代理几乎可以是任何对象:文件,资源,内存中的对象,或者一些难以复制的东西。Vue3的响应式原理就用到Proxy,代理模式。

class Star{
    play(){
        console.log("演戏")
    }
}

class StarProxy{
    constructor(){
        this.supperStar = new Star()
    }
    talk(price){
        if(price >= 10000){
            this.supperStar.play()
        }else{
            throw new Error("价钱不合适")
        }
    }
}

let jr = new StarProxy()
jr.talk(10000)

9.观察者模式

观察者模式包含观察目标和观察者两类对象,一个目标可以有任意数目的与之相依赖的观察者,一旦观察目标的状态发生变化,所有观察者都将得到通知。

当一个对象的状态发生变化时,所有依赖于它的对象都得到通知并被自动更新,解决了主体对象与观察者之间功能的耦合,即一个对象状态改变给其他对象通知的问题。

观察者模式可以应用在面包屑等。

优势:目标者和观察者,功能耦合度降低,专注自身功能逻辑;观察者被动接收更新,时间上解耦,实时接收目标者更新状态。

缺点:观察者模式虽然实现了对象依赖关系的低耦合,但却不能对事件通知进行细分管控,如“筛选通知”,“指定主题事件通知”。

class Subject{
    constructor(){
        this.observers
    }
    add(observer){
        this.observers.push(observer)
    }
    remove(observer){
        this.observers = this.observers.filter(item => item !== observer)
    }
    notify(){
        this.observers.forEach(item => {
            item.update()
        })
    }
}

class Observer{
    constructor(name){
        this.name = name
    }
    update(){
        console.log("update", this.name)
    }
}

const subject = new Subject()
const observer1 = new Observer('name1')
const observer2 = new Observer('name2')
subject.add(observer1)
subject.add(observer2)

10.发布订阅模式

观察者和目标要互相知道

发布者和订阅者不需要互相知道,通过第三方实现调度,属于经过解耦合的观察者模式。

// publish 发布
// subscribe 订阅
const PubSub = {
    message: {},
    publish(type, data){
        if(!this.message[type]) return
        this.message[type].forEach(item => item(data))
    },
    subscribe(type, cb){
        if(!this.message[type]){
            this.message[type] = [cb]
        }else{
            this.message[type].push(cb)
        }
    },
    unsubscribe(type, cb){
        if(!this.message[type]) return
        if(!cb){
            this.message[type].length = 0
        }else{
            this.message[type] = this.message[type].filter(item => item !== cb)
        }
    }
}

function testA(data){
    console.log("testA", data)
}
function testB(data){
    console.log("testB", data)
}
PubSub.subcribe("A", testA)
PubSub.subcribe("B", testB)

11.模块模式

模块化模式最初被定义为在传统软件工程中为类提供私有和公有封装的一种方法。

能够使一个单独的对象拥有私有/公有方法和变量,从而屏蔽来自全局作用域的特殊部分。这可以减少我们的函数名与在页面中其他脚本区域内函数定义的函数名冲突的可能性。

// 闭包
var testModule = (function(){
    var count = 0
    return {
        increment: function(){
            return ++count
        },
        reset: function(){
            count = 0
        },
        decrement(){
            return --count
        }
    }
})()
// ES6模块化
let count = 0
function increment(){
    return ++count
}
function decrement(){
    return --count
}

export default {
    increment,
    decrement
}

module模式使用了闭包封装“私有”状态和组织。它提供了一种包装混合公有/私有方法和变量的方式,防止泄露至全局作用域,并与别的开发人员的接口发生冲突。通过该模式,只需要返回一个公有的API,而其他的一切则都维持在私有闭包里。

12.桥接模式

将抽象部分与它的实现部分分离,使它们都可以独立的变化。

使用场景:一个类存在两个或多个独立变化的维度,且这两个维度都需要进行扩展。

优点:把抽象与实现隔离开,有助于独立的管理各组成部分。

缺点:每使用一个桥接元素都要增加一次函数调用,这对应用程序的性能会有一些负面影响——提高了系统的复杂度。

// 桥接铭泰
function Audi1(engine){
    this.engine = engine
}

Audi1.prototype.platform = function(){
    console.log("audi1平台")
}

Audi1.prototype.loadEngine = function(){
    this.engine.run()
}

function Audi(2engine){
    this.engine = engine
}

Audi2.prototype.platform = function(){
    console.log("audi1平台")
}

Audi2.prototype.loadEngine = function(){
    this.engine.run()
}

function V6(){
    this.run = function(){
        console.log("v6 发动机")
    }
}
function V8(){
    this.run = function(){
        console.log("v8 发动机")
    }
}

let aodi1v6 = new Audi1(new V6())
let audi1v8 = new Audi1(new V8())
let audi2v8 = new Audi2(new V8())

audi1v6.loadEngine()
audi1v8.loadEngine()
audi2v8.loadEngine()

应用:例如弹框的动画效果

// Toast
// Message
// Modal
// 以上三种类型的信息展示形式,Toast/Message/Modal的show和hide抽象出来,让Animations去具体实现
const Animations = {
    bounce:{
        show(ele){
            console.log(ele, "弹跳显示")
        },
        hide(ele){
            console.log(ele, "弹跳隐藏")
        }
    },
    slide:{
        show(ele){
            console.log(ele, "滑动展示")
        },
        hide(ele){
            console.log(ele, "滑动隐藏")
        }
    }
}

function Toast(ele, animation){
    this.ele = ele
    this.animation = animation
}
Toast.prototype.show = function(){
    //抽象和实现隔离,Toast的show是抽象方法,animation的show是具体实现
    this.animation.show()
}
Toast.prototype.hide = function(){
    this.animation.hide()
}

let toast1 = new Toast("div1", Animation.bounce)
toast1.show()

13.组合模式

组合模式在对象间形成树形结构

组合模式中基本对象和组合对象被一致对待

无需关心对象有多少层,调用时,只需在根部进行调用。

它在我们树形结构的问题中,模糊简单元素和复杂元素的概念,客户程序可以像处理简单元素一样来处理复杂元素,从而使得客户程序和复杂元素内容结构解耦。

const Folder = function(folder){
    this.folder = folder
    this.list = [] // 保存子文件夹或者子文件
}
Folder.prototype.add = function(res){
    this.list.push(res)
}
Folder.prototype.scan = function(){
    console.log("扫描文件夹", this.folder)
    for(let i = 0; i < this.list.length; i++){
        this.list[i].scan()
    }
}

const File = function(file){
    this.file = file
}
File.prototype.scan = function(){
    console.log("扫描文件夹", this.file)
}

//根
let rootFolder = new Folder("root")
//子文件夹
let htmlFolder = new Folder("html")
let jsFolder = new Folder("js")
let cssFolder = new Folder("css")
//文件
let html4 = new File("html4")
let html5 = new File("html5")
let js2 = new File("js2")
let js3 = new File("js3")
let css6 = new File("css6")
let css7 = new File("css7")

rootFolder.add(htmlFolder)
rootFolder.add(jsFolder)
rootFolder.add(cssFolder)

htmlFolder.add(html4)
htmlFolder.add(html5)
jsFolder.add(js2)
jsFolder.add(js3)
cssFolder.add(css6)
cssFolder.add(css7)

rootFolder.scan()

14.命令模式

有时候需要对某些对象发送请求,但是并不知道请求的接收者是谁,也不知道被请求的操作是什么。需要一种松耦合的方式来设计程序,使得发送者和接收者能够消除彼此之间的耦合关系。

命令模式由三种角色构成:

1.发送者invoker(发出命令,调用命令对象,不知道如何执行,与谁执行)

2.接收者receiver(提供对应接口处理请求,不知道谁发起请求)

3.命令对象command(接收命令,调用接收者对应接口处理发布者的请求)

class Receiver {
    //接收类
    execute(){
        console.log("接收者执行请求")
    }
}
class Command {
    //命令类
    constructor(receiver){
        this.receiver = receiver
    }
    execute(){
        console.log("命令对象=>接收者如何处理")
        this.receiver.execute()
    }
}
class Invoker {
    //发布类
    constructor(command){
        this.command = command
    }
    execute(){
        console.log("发布请求")
        this.command.execute()
    }
}

const command = new Command(new Receiver())
const invoker = new Invoker(command)
invoker.execute()

组合模式和命令模式结合的例子:

class MacroCommand {
    constructor(){
        this.list = [] //子命令对象
    }
    add(command){
        this.list.push(command)
    }
    execute(){
        for(let item of this.list){
            item.execute()
        }
    }
}

const Tab = {
    execute(){
        console.log("选项卡执行")
    }
}

const Swipe = {
    execute(){
        console.log("轮播图执行")
    }
}

const macroCommand = new MacroCommand()
macroCommand.add(Tab)
macroCommand.add(Swipe)
macroCommand.execute()

15.模板方法模式

模板方法模式由两部分组成,第一部分是抽象父类,第二部分是具体的实现子类。通常在抽象父类中封装了子类的算法框架,包括实现一些公共方法以及封装子类中方法的执行顺序。子类通过继承这个抽象类,也继承了整个算法结构,并且可以选择重写父类的方法。

var Container = function(params){
    var F = function(){}
    F.prototype.init = function(){
        let data = this.getData()
        this.render(data)
    }
    F.prototype.getData = params.getData || function(){
        throw new Error("必须传入getData")
    }
    F.prototype.render = function(data){
        console.log("render", data)
    }
    return F
}

var MyClass = Container({
    getData(){
        console.log("获取数据")
        return [1,2,3]
    }
})
new MyClass().init()

模板方法模式是一种典型的通过封装变化提高系统扩展性的设计模式。在运用了模板方法模式的程序中,子类方法的种类和执行顺序都是不变的,但是子类的方法具体实现规则是可变的。父类是个模板,子类可以添加,就增加了不同的功能。

16.迭代器模式

迭代器模式是指提供一种方法顺序访问一个聚合对象中的各个元素,而又不需要暴露该对象的内部表示。迭代器模式可以把迭代的过程从业务逻辑中分离出来,在使用迭代器模式后,即使不关心对象的内部构造,也可以按顺序访问其中的每个元素。

1.为遍历不同数据结构的“集合”提供统一的接口

2.能遍历访问“集合”数据的项,不关心项的数据结构

var myEach = function(arr, callback){
    for(let i = 0; i < arr.length; i++){
        callback(i, arr[i])
    }
}

myEach([11,22,33,44], function(key, value){
    console.log(key,value)
})

ES6的迭代器:

var obj = {
    name:"实现迭代器"list:["aaa", "bbb", "ccc"],
    [Symbol.iterator]:function(){
        let index = 0
        return {
            // 为保证里边的this和外边的this一个指向,需要用箭头函数
            next: () => {
                if(index < this.list.length){
                    return {
                        value:this.list[index++],
                        done:false
                    }
                }else{
                    return {
                        value: undefined,
                        done: true
                    }
                }
            }
        }
    }
}

var it = obj[Symbol.iterator]
console.log(it.next())
console.log(it.next())
console.log(it.next())
console.log(it.next())

17.职责链模式

使多个对象都有机会处理请求,从而避免了请求的发送者与多个接收者直接的耦合关系,将这些接收者连接成一条链,顺着这条链传递该请求,直到找到能处理该请求的对象。有点像原型链。

function checkEmpty(){
    if(input.value.length === 0){
        console.log("这里不能为空")
        return
    }
    return "next"
}
function checkNumber(){
    if(Number.isNaN(+input.value)){
        console.log("这里必须是数字")
        return
    }
    return "next"
}
function checkLength(){
    if(input.value.length < 6){
        console.log("这里必须要大于6个数字")
        return
    }
    return "next"
}
class Chain {
    constructor(fn){
        this.checkRule = fn
        this.nextRule = null
    }
    addRule(nextRule){
        this.nextRule = new Chain(nextRule)
        // 为方便链式调用,将next返回出去
        return this.nextRule
    }
    // 避免最后一个规则不是函数导致报错,添加一个返回end的方法
    end(){
        this.nextRule = {
            check: () => "end"
        }
    }
    check(){
        this.checkRule() === "next" ? this.nextRule.check():null
    }
}

const checks = new Chain(checkEmpty)
// 避免最后一个规则不是函数导致报错,添加一个返回end的方法
checks.addRule(checkNumber).addRule(checkLength).end()

优点:

1.符合单一职责,使每个方法中都只有一个职责。

2.符合开放封闭原则,在需求增加时可以很方便的扩充新的责任。

3.使用时候不需要知道谁才是真正处理的方法,减少大量的if或switch语法。

缺点:

1.团队成员需要对责任链存在共识,否则当看到一个方法莫名其妙返回一个next时一定会很奇怪。

2.出错时不好排查问题,因为不知道到底在那个责任中出的错,需要从链头开始往后找。