js中常用设计模式(持续更新)

91 阅读10分钟

为什么要了解用设计模式

1.写代码要有良好的封装,要高内聚,低耦合

2.设计模式为了解决在软件设计、开发过程中,针对特定场景、特定问题的较优解决方案

3.不使用设计模式进行开发容易造成,因设计缺陷、代码实现缺陷,给后期维护、开发、迭代带来了麻烦。

什么是高内聚,低耦合?

即五大基本原则(SOLID)的简写

高层模块不依赖底层模块,即为依赖反转原则。

内部修改关闭,外部扩展开放,即为开放封闭原则。

聚合单一功能,即为单一功能原则。

低知识要求,对外接口简单,即为迪米特法则。

耦合多个接口,不如独立拆分,即为接口隔离原则。

合成复用,子类继承可替换父类,即为里式替换原则

为什么要封装代码?

我们可以观察React、Vue、EventEmitter、Axios等等这些优秀的源码,会发现其实他们封装的模块都是有迹可循的。这些规律总结起来就是设计模式。

  1. 良好的封装,不会让内部变量污染外部
  1. 封装好的代码可以作为一个模块给外部调用。外部无需了解细节,只需按约定的规范调用。
  1. 对扩展开放,对修改关闭,即开放关闭原则。外部不能修改内部代码,保证了内部的正确性;又留出扩展接口,提高了灵活性。

模式类型

模式类型设计模式
创建型模式单例模式、工厂模式、建造者模式
结构型模式适配器模式、装饰器模式、代理模式
行为型模式策略模式、观察者模式、发布订阅模式、职责链模式、中介者模式

观察者模式

image.png

特征

观察者模式:定义了对象间一种一对多的依赖关系,当目标对象 Subject 的状态发生改变时,所有依赖它的对象 Observer 都会得到通知。

一个目标者对象 Subject, 拥有方法: 添加 / 删除 / 通知 Observer;

多个观察者对象 Observer, 拥有方法: 接收 Subject 状态变更通知并处理;

目标对象 Subject 状态变更时, 通知所有 Observer。

Subject 添加一系列 Observer, Subject 负责维护与这些 Observer 之间的联系,“ 你对我有兴趣, 我更新就会通知你”。

优缺点

优点 目标变化就会通知观察者,这是观察者模式最大的优点 缺点 不灵活。目标和观察者是耦合在一起的,要实现观察者模式,必须同时引入被观察者和观察者才能达到响应式的效果。

观察者模式示例:

   //  目标者类
    class Subject {
        constructor() {
                this.observers = []; //观察者列表

            }
            //  添加
        add(observer) {
                this.observers.push(observer);
            }
            // 删除
        remove(observer) {
                let idx = this.observers.findIndex(item => item === observer);
                idx > -1 && this.observers.splice(idx, 1);
            }
            //通知
        notify() {
            for (let observer of this.observers) {
                observer.update();

            }
        }
    }
    // 观察者类
    class Observer {
        constructor(name) {
                this.name = name;

            }
            //目标对象更新时触发的回调
        update() {
            console.log(`目标者通知我更新了,我是:${this.name}`)
        }
    }

    // 实例化目标者
    let subject = new Subject();
    console.log(subject, 'subject')
        // 实例化两个观察者
    let obs1 = new Observer('前端开发者');
    let obs2 = new Observer('后端开发者');
    // 向目标者添加观察者
    subject.add(obs1);
    subject.add(obs2);
    console.log(subject, 'subject2')
        // 目标者通知更新
    subject.notify();

问题:不能单独通知一个比如只通知前端开发者,两个观察者接收目标者状态变更通知后,都执行了 update(),并无区分。所以进阶版的观察这模式也可称为发布订阅者模式,可以解决这个问题

例子:领导(被观察者)在台上介绍防疫政策,底下的工作人员(观察者)“观察”领导说的防疫政策的变化,当政策变化时,通知(update)到街道。

 let observerIds = 0;
    //被观察者
    class Subject {
        constructor() {
                this.observers = [];
            }
            //添加观察者
        addObserver(observer) {
                this.observers.push(observer)
            }
            //移除观察者
        removeObserver(observer) {
                this.observers = this.observers.filter((obs) => {
                    return obs.id !== observer.id;
                })
            }
            //通知notify观察者
        notify() {
            this.observers.forEach((observer) => {
                observer.update(this)
            })
        }
    }
    //观察者 
    class Observer {
        constructor() {
            this.id = observerIds++
        }
        update(Subject) {
            //  更新
            console.log(`领导通知我更新了,政策变化了,我是:工作人员${this.id}`)
        }
    }

    let subject = new Subject();
    console.log(subject, 'subject')
        // 实例化两个观察者
    let obs1 = new Observer();
    let obs2 = new Observer();
    // 向目标者添加观察者
    subject.addObserver(obs1);
    subject.addObserver(obs2);
    console.log(subject, 'subject2')
        // 目标者通知更新
    subject.notify();

发布订阅者模式

image.png

基于一个事件(主题)通道,希望接收通知的对象 Subscriber 通过自定义事件订阅主题,被激活事件的对象 Publisher 通过发布主题事件的方式通知各个订阅该主题的 Subscriber 对象

顺序 :先订阅后发布时才通知(常规)

使用场景

1.可以实现更细粒度的一些控制。比如发布者发布了很多消息,但是不想所有订阅者都接收到,就可以在调度中心做一些处理,类似于权限控制之类的。还可以做一些节流操作。 示例:

 // 事件中心
   let pubSub = {
           list: {},
           //  订阅
           subscribe: function(key, fn) {
               if (!this.list[key]) {
                   this.list[key] = [];
               }
               this.list[key].push(fn);
               console.log(this.list, 'this.list')

           },
           //  发布
           publish: function(key, ...arg) {
               for (let fn of this.list[key]) {

                   fn.call(this, ...arg)

               }
           },
           //取消订阅
           unSubscribe: function(key, fn) {
               let fnList = this.list[key];
               if (!fnList) return false;
               if (!fn) {
                   // 不传入指定的取消订阅方法,则青空所有key下的订阅
                   fnList && (fnList.length = 0)
               } else {
                   fnList.forEach((item, index) => {
                       if (item === fn) {
                           fnList.splice(index, 1);
                       }
                   })
               }

           }
       }
       // 订阅
   pubSub.subscribe('onwork', time => {
       console.log(`上班了:${time}`)
   })
   pubSub.subscribe('offwork', time => {
       console.log(`下班了:${time}`)
   })
   pubSub.subscribe('oneat', time => {
           console.log(`吃饭了:${time}`)
       })
       // 发布
   pubSub.publish('onwork', '18:00:00')
   pubSub.publish('oneat', '12:00:00')
       // 取消订阅
   pubSub.unSubscribe('onwork')

2.DOM 事件监听也是 “发布订阅模式” 的应用: image.png

3.阅后可获取过往以后的发布通知 (QQ离线消息,上线后获取之前的信息)

4.流行库的应用 jQuery 的 on 和 trigger,$.callback();

Vue 的双向数据绑定;

Vue 的父子组件通信 on/on/emit

5.我们微信会关注很多公众号,公众号有新文章发布时,就会有消息及时通知我们文章更新了。

这个时候公众号为发布者,用户为订阅者,用户将订阅公众号的事件注册到事件调度中心,当发布者发布新文章时,会发布事件至事件调度中心,调度中心会发消息告诉订阅者。

示例代码:

 class Event {
        constructor() {
                this.eventEmitter = {}
            }
            //  订阅
        on(type, fn) {
                if (!this.eventEmitter[type]) {
                    this.eventEmitter[type] = []
                }
                this.eventEmitter[type].push(fn)
            }
            // 取消订阅
        off(type, fn) {
                if (!this.eventEmitter[type]) {
                    return;
                }
                this.eventEmitter[type] = this.eventEmitter[type].filter((event) => {
                    return event !== fn
                })
            }
            // 发布
        emit(type) {
            if (!this.eventEmitter[type]) {
                return
            }
            this.eventEmitter[type].forEach(event => {
                event();
            });
        }
    }

观察者模式和发布订阅模式的区别

1.观察者模式中,观察者和目标直接进行交互,观察者由具体目标调度,每个被订阅的目标里面都需要有对观察者的处理。

2.观察者模式和发布订阅模式都是为了解耦,减少代码的冗余,不同的是,观察者模式中观察者必须知道被观察者,而发布订阅模式解耦更彻底,订阅者与发布者不需要相互知道,只需要向事件大厅订阅和发布即可。

3.发布订阅模式与观察者模式的不同,“第三者” (事件中心)出现。目标对象并不直接通知观察者,而是通过事件中心来派发通知。

  1. 发布订阅模式中,订阅者各自实现不同的逻辑,且只接受自己对应的事件通知。实现你想要的 “不一样”

单例模式

image.png; 单例模式也称为单体模式,规定一个类只有一个实例,并且提供可全局访问点;

单例模式的特点是”唯一“和”全局访问“,那么我们可以联想到JavaScript中的全局对象,全局对象是最简单的单例模式;

(1)一个实例只生产一次 (2)保证一个类仅有一个实例,并提供一个访问它的全局访问点

  • Singleton :特定类,这是我们需要访问的类,访问者要拿到的是它的实例;
  • instance :单例,是特定类的实例,特定类一般会提供 getInstance 方法来获取该单例;
  • getInstance :获取单例的方法; 示例代码
    // 单例模式
    class Singleton {
        static _instance = null;
        static getInstance() {

            if (!Singleton._instance) {

                Singleton.instance = new Singleton();
            }
            //如果这个唯一的实例已经存在,则直接返回

            return Singleton._instance
        }
    }
    const s1 = Singleton.getInstance();
    const s2 = Singleton.getInstance();
    console.log(s1 === s2)

作用

优缺点

优点:  节约资源,保证访问的一致性。

缺点:  扩展性不友好,因为单例模式一般自行实例化,没有接口。

应用场景

如果一个类实例化过程消耗资源比较多,可以使用单例避免性能浪费

需要公共状态,可以使用单例保证访问一致性 (1)登录页面中的浮窗

(2)Vuex:实现了一个全局的store用来存储应用的所有状态。这个store的实现就是单例模式的典型应用。

let Vue // instance 实例

export function install (_Vue) {
 // 判断传入的Vue实例对象是否已经被install过(是否有了唯一的state)
 if (Vue && _Vue === Vue) {
   if (process.env.NODE_ENV !== 'production') {
     console.error(
       '[vuex] already installed. Vue.use(Vuex) should be called only once.'
     )
   }
   return
 }
 // 若没有,则为这个Vue实例对象install一个唯一的Vuex
 Vue = _Vue
 // 将Vuex的初始化逻辑写进Vue的钩子函数里
 applyMixin(Vue)
}
通过这种方式,可以保证一个 Vue 实例只会被 install 一次 Vuex 插件,所以每个 Vue 实例只会拥有一个全局的 Store

其它

四种单例模式的实现可参考 www.jb51.net/article/210…

工厂模式

根据不同的参数,返回不同类的实例。

核心思想:将对象的创建与对象的实现分离。实现复杂,但使用简单。工厂会给我们提供一个工厂方法,我们直接去调用即可。

工厂模式是用来创建对象的一种最常用的设计模式。我们不暴露创建对象的具体逻辑,而是将将逻辑封装在一个函数中,那么这个函数就可以被视为一个工厂。工厂模式根据抽象程度的不同可以分为:简单工厂,工厂方法和抽象工厂。 工厂模式是最常见的一种开发模式,将new操作单独封装,当遇到new是应考虑工厂模式。创建对象,不暴露代码逻辑,把逻辑写到函数里面。这个函数就是工厂模式。优点,简单。   缺点就是,每增加构造函数,都要修改函数里面的代码逻辑。

(1) 简单工厂模式 简单工厂模式又叫静态工厂模式,由一个工厂对象决定创建某一种产品对象类的实例。主要用来创建同一类对象。

在实际的项目中,我们常常需要根据用户的权限来渲染不同的页面,高级权限的用户所拥有的页面有些是无法被低级权限的用户所查看。所以我们可以在不同权限等级用户的构造函数中,保存该用户能够看到的页面。在根据权限实例化用户。代码如下:

class SuperAdmin{
  constructor(){
    this.name = "超级管理员";
    this.viewPage = ['首页''通讯录''发现页''应用数据''权限管理'];
  }
}
class Admin{
  constructor(){
    this.name = "管理员";
    this.viewPage = ['首页''通讯录''发现页''应用数据''权限管理'];
  }
}
class NormalUser{
  constructor(){
    this.name = "普通用户";
    this.viewPage = ['首页''通讯录''发现页''应用数据''权限管理'];
  }
}
//工厂方法类
class UserFactory {
  getFactory(role){
   switch (role) {
     case 'superAdmin':
       return new SuperAdmin();
       break;
     case 'admin':
       return new Admin();
       break;
     case 'user':
       return new NormalUser();
       break;
     default:
       throw new Error('参数错误, 可选参数:superAdmin、admin、user');
   }
  }
 }
 
//调用
let uesr =new UserFactory();
uesr.getFactory('superAdmin');
uesr.getFactory('admin');
uesr.getFactory('user');
 class User {
 //构造器
 constructor(opt) {
   this.name = opt.name;
   this.viewPage = opt.viewPage;
 }
 //静态方法
 static getInstance(role) {
   switch (role) {
     case 'superAdmin':
     return new User({ name: '超级管理员', viewPage: ['首页', '通讯录', '发现页', '应用数据', '权限管理'] });
       break;
     case 'admin':
       return new User({ name: '管理员', viewPage: ['首页', '通讯录', '发现页', '应用数据'] });
       break;
     case 'user':
       return new User({ name: '普通用户', viewPage: ['首页', '通讯录', '发现页'] });
       break;
     default:
       throw new Error('参数错误, 可选参数:superAdmin、admin、user')
   }
 }
}

//调用
let superAdmin = User.getInstance('superAdmin');
let admin = User.getInstance('admin');
let normalUser = User.getInstance('user');

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

使用场景

(1)Vue、React 源码中的工厂模式document.createElement 创建 DOM 元素。这个方法采用的就是工厂模式,方法内部很复杂,但外部使用很简单。只需要传递标签名,这个方法就会返回对应的 DOM 元素。

(2)使用者只需要知道产品名字就可以拿到实例,不关心创建过程。所以我们可以把复杂的过程封装在一块,更便于使用。

image.png

例如:我们去环球影城的餐厅吃饭,点了一份“牛肉拉面”、“馄饨云吞面”,面煮好了,就直接端到桌子上,我们只管吃,不用在乎煮面的过程。

这个过程中,我们扮演访问者的角色,餐厅扮演的就是工厂的角色,“xxx”面就是产品。

class restaurant {
        constructor() {
                this.menuData = {}
            }
            // 获取菜品
        getDish(dish) {
                if (!this.menuData[menu]) {
                    console.log('菜品不存在,获取失败');
                    return;
                }
                return this.menuData[menu]
            }
            //添加菜品
        addMenu(menu, description) {
                if (this.menuData[menu]) {
                    console.log('菜品存在,请勿重复添加')
                    return;
                }
                this.menuData = menu;
            }
            //移除菜品
        removeMenu(menu) {
            if (!this.menuData[menu]) {
                console.log('菜品不存在,移除失败')
            }
            delete this.menuData[menu]
        }
    }
    class Dish {
        constructor(name, description) {
            this.name = name;
            this.description = description;
        }
        eat() {
            console.log(`i m eating ${this.name},its ${description
            }`)
        }
    }

适配器模式

装饰器模式

策略模式