JavaScript设计模式

探讨一些经典和现代的设计模式的JavaScript实现。

没有最好的,只有最合适的

设计模式一共分为3大类23种

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

单例模式

一个类只有一个实例,并提供一个访问他的全局访问点

image-20230319225821142

  • Singleton :特定类,这是我们需要访问的类,访问者要拿到的是它的实例;

  • instance :单例,是特定类的实例,特定类一般会提供 getInstance 方法来获取该单例;

  • getInstance :获取单例的方法;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Singleton {
let _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) // true

实例

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

使用场景

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

  2. 需要公共状态,可以使用单例保证访问一致性。

工厂模式

工厂模式:根据不同的参数,返回不同类的实例。将对象的创建与对象的实现分离。实现复杂,但使用简单。直接使用工厂提供的方法即可

优点:

  1. 良好的封装,访问者无需了解创建过程,代码结构清晰。

  2. 扩展性良好,通过工厂方法隔离了用户和创建流程,符合开闭原则。

缺点:

给系统增加了抽象性,带来了额外的系统复杂度,不能滥用

实例

document.createElement 创建 DOM 元素。这个方法采用的就是工厂模式,方法内部很复杂,但外部使用很简单。

使用场景

  • 对象创建比较复杂,访问者无需了解创建过程。

  • 需要处理大量具有相同/类似属性的小对象。

适配器模式

用于解决兼容问题,接口/方法/数据不兼容,将其转换成访问者期望的格式进行使用。

image-20230319230730502

场景特点:

  1. 整合第三方SDK
  2. 封装旧接口

装饰器模式

  • 动态地给某个对象添加一些额外的职责,,是一种实现继承的替代方案
  • 在不改变原对象的基础上,通过对其进行包装扩展,使原有对象可以满足用户的更复杂需求,而不会影响从这个类中派生的其他对象

image-20230319230958645

有点原型链的味道

代理模式

为一个对象提供一个代用品或占位符,以便控制对它的访问

使用场景

  • ES6的proxy
  • jQuery.proxy()方法

装饰者与代理模式的区别

  • 装饰者模式: 扩展功能,原有功能不变且可直接使用
  • 代理模式: 显示原有功能,但是经过限制之后的

策略模式

定义一系列算法,根据输入的参数决定使用哪个算法。

image-20230319231627454

实例

场景:双十一满减活动。满200-20、满300-50、满500-100。这个需求,怎么写?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// if-else:臃肿,难改动
function priceCalculate(discountType,price){
if(discountType === 'discount200-20'){
return price - Math.floor(price/200) * 20;
}else if(discountType === 'discount300-50'){
return price - Math.floor(price/300) * 50;
}else if(userType === 'discount500-100'){
return price - Math.floor(price/500) * 100;
}
}
//策略模式改写,隐藏了算法,预留了增加策略的入口,便于拓展

const priceCalculate = (function(){
const discountMap = {
'discount200-20': function(price) {
return price - Math.floor(price / 200) * 20;
},
'discount300-50': function(price) {
return price - Math.floor(price/300) * 50;
},
'discount500-100': function(price) {
return price - Math.floor(price/500) * 100;
},
};
return {
addStategy(stategyName,fn){
if(discountMap[stategyName]) return;
discountMap[stategyName] = fn;
},
priceCal(discountType,price){
return discountMap[discountType] && discountMap[discountType](price);
}
}
})()

优点:

  1. 策略相互独立,可以互相切换。提高了灵活性以及复用性。
  2. 不需要使用if-else进行策略选择,提高了维护性。
  3. 可扩展性好,满足开闭原则。

缺点:

  1. 策略相互独立,一些复杂的算法逻辑无法共享,造成资源浪费。
  2. 用户在使用策略时,需要了解具体的策略实现。不满足最少知识原则,增加了使用成本。

使用场景

  1. 算法需要自由切换的场景。
  2. 多个算法只有行为上有些不同,可以考虑策略模式动态选择算法。
  3. 需要多重判断,可以考虑策略模式规避多重条件判断。

观察者模式

一个对象(称为subject)维持一系列依赖于它的对象(称为observer),将有关状态的任何变更自动通知给它们(观察者)。

优缺点

优点:目标变化就会通知观察者,这是观察者模式最大的优点。

缺点: 目标和观察者是耦合在一起的,要实现观察者模式,必须同时引入被观察者和观察者才能达到响应式的效果。

使用场景

假设B站用户就是观察者,B站up主是被观察者,有多个的B站用户关注了青春湖北这个up主,当这个up主更新视频时就会通知这些关注的B站用户。

发布订阅模式

基于一个主题,希望接收通知的对象(称为subscriber)通过自定义事件订阅主题,被激活事件的对象(称为publisher)通过发布主题事件的方式被通知。

使用场景

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

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

Vue 双向绑定中的发布订阅模式

image-20230319232727395

Vue 双向绑定通过数据劫持和发布-订阅模式实现

  • 通过DefineProperty劫持各个数据的settergetter,并为每个数据添加一个订阅者列表,这个列表将会记录所有依赖这个数据的组件。

    响应式数据相当于消息的发布者。

  • 每个组件都对应一个Watcher订阅者,当组件渲染函数执行时,会将本组件的Watcher加入到所依赖的响应式数据的订阅者列表中。

    这个过程叫做“依赖收集”。

  • 当响应式数据发生变化时,会出settersetter负责通知数据的订阅者列表中的WatcherWatcher触发组件重新渲染来更新视图。

    视图层相当于消息的订阅者。

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

观察者是经典软件设计模式中的一种,但发布订阅只是软件架构中的一种消息范式

观察者模式 发布订阅
2个角色 3个角色
重点是被观察者 重点是发布订阅中心

观察与被观察的关系是通过被观察者主动建立的,被观察者至少要有三个方法——添加观察者、移除观察者、通知观察者。

发布订阅基于一个中心来建立整个体系,其中发布者订阅者不直接进行通信,而是发布者将要发布的消息交由中心管理,订阅者也是根据自己的情况,按需订阅中心中的消息。

发布订阅的实现内部利用了观察者模式,但由于发布订阅中心这一中间层的出现,对于生产方和消费方的通信管理变得更加的可管理和可拓展。

理解【观察者模式】和【发布订阅】的区别 - 掘金 (juejin.cn)

JS 常用的六种设计模式介绍 - 掘金 (juejin.cn)