前端常用的几种设计模式及代码实现

244 阅读6分钟

设计模式是一种书写代码的方式,为了解决特定问题给出的简洁而优化的解决方案

常用的几种设计模式

1.单例模式

核心:一个构造函数一生只能有一个实例,不管你new多少次,都是这一个实例。

应用如:自定义弹出层

我们分析一下如下代码:

function Person(){
  this.name = 'jack'
}
const p1 = new Person()
const p2 = new Person()
console.log(p1 === p2) // false

以上代码的问题在于p1 === p2返回true时才表示实现了单例模式

所以我们先来写一个单例模式的核心代码

let instance = null
function singleton() {
  if (!instance) instance = 实例对象
  return instance
}

加入单例模式核心代码后:

function Person() {
  this.name = 'jack'
}
let isntance = null
function singleton() {
  if (!instance) instance = new Person()
  return instance
}
const p1 = singleton()
// 当第二次调用singleton的时候,此时instance是第一次new出来的实例,if条件就不会执行了,直接返回第一次实例的地址
const p2 = singleton()
console.log(p1 === p2) // true

以上代码的问题:

  • 书写代码的时候和构造函数已经没有关系了,直接调用了封装的singleton函数
  • new关键字没有了 所以,让我们改造一下单例模式的代码,改造核心如下:
  • 需要把instance变量保存下来
  • singleton是一个函数,里边可以判断 可以返回
  • 以闭包的形式来执行一段代码
  • 为了保存构造函数,把它也写到闭包里边
  • 如果使用需要new,那么就直接写
const Person = (function() {
  // 真实构造函数体
  function Person(name, age, gender) {
    this.name = 'jack'
    this.age = age
    this.gender = gender
  }
  Person.prototype.sayHi = function() {
  	console.log('hello world')
  }
  // 这个变量因为在一个不会被销毁的函数执行空间里边,所以会一直存在
  let instance = null
  return function singleton(...arg) {
    if (!instance) instance = new Person(...arg)
    return instance
  }
})()
const p1 = new Person('jack', 18, '男')
const p2 = new Person('rose', 19, '女') // 此处传参无用
console.log(p1 === p2) // true

2.观察者模式

核心:观察者模式主要是监控一个对象的状态,一旦状态发生变化,就触发一些技能。需要2个构造函数来实现(一个创建观察者,一个创建被观察者)

例子:监控

=》我们坐在教室里,就是被观察者
=》坐在监控后边的老师就是观察者
=》当被观察者出发了一些条件的时候,观察者就会触发一些技能

创建观察者:需要一个身份证明和一个技能

// 观察者构造函数
class Observer {
  constructor(name, fn = () => {}) {
    this.name = name
    this.fn = fn
  }
}
// 创建两个观察者实例
const bzr = new Observer('班主任', (state) => { console.log(`因为 ${state} 把你家长找来`) })
const xz = new Observer('校长', (state) => { console.log(`开会`) })

创建被观察者:

  • 需要一个属性,记录自己的状态。
  • 需要一个队列,记录都有谁观察着自己。数组[]
  • 方法:能设置自己的状态,当需要改变状态的时候,触发这个方法。
  • 方法:添加观察者。
  • 方法:删除观察者。
// 被观察者构造函数
class subject {
  constructor(state) {
    this.state = state // 记录状态
    this.observers = [] // 记录观察着我的人
  }
  // 设置状态的函数
  setState(val) {
    this.state = val
    // 状态一变,就需要触发所有观察者的技能
    // 遍历observers依次触发技能
    this.observers.forEach(item => {
      // 告诉改变成了什么状态
      item.fn(this.state)
    })
  }
  // 添加观察者
  addObserver(obs) {
    // 谁是观察者就传递谁进来
    // 判断一下,如果观察者已存在,就不再添加
    this.observers = this.observers.filter(item => item !== obs)
    this.observers.push(obs)
  }
  // 删除观察者
  delObserver(obs) {
    this.observers = this.observers.filter(item => item !== obs)
  }
}
// 创建一个被观察者
const xiaoming = new subject('学习')
// 给xiaoming添加观察者
xiaoming.addObserver(bzr)
xiaoming.addObserver(xz)
console.log(xiaoming)

3.发布订阅模式

核心:

  • 有一个对象,有人一直看着它。
  • 当这个对象发变化的时候,第三方通知这个看着的人,触发技能。
  • 只需要创建一个第三方的构造函数即可。 (addEventListener也属于发布订阅模式,相当于监控着dom元素的变化)

例子:买书

1.普通程序员买书:

=》去书店,问,没有,回家
=》过一会再去,问,没有,回家
=》过一会再去,问,没有,回家

2.发布订阅程序员:

=》去书店,问,没有,留下一个联系方式给店员
=》一旦有了书,就会接到电话
=》触发技能去买书

接下来让我们分析一下构造函数中的内容:

  • 需要一个任务队列如:
{
  click: [ fn1, fn2 ],
  abc: [ fn1, fn2 ],
}
  • 方法:能向消息队列里边添加内容
  • 方法: 可删除消息队列里边的内容
  • 方法:能触发消息队列里面的内容
// 创建一个第三方观察者构造函数
class Observer {
  constructor() {
    this.message = {} // 存放消息队列
  }
  // 向消息队列里边添加内容
  on(type, fn) {
    // type: 拜托看着的行为
    // fn: 在行为发生的时候做的事情
    // 就应该把这些记录在消息队列里边:
    // 判断message里边有没有这个行为被注册过了
    // 如果没有直接赋值一个这个行为
    // 如果有,直接向数组里添加就可以了
    if (!this.message[type]) {
    	this.message[type] = []
    }
    // 这块是不会去重重复的操作的。就像可以用addEventListener绑定多个相同的事件一样。一般业务逻辑里也不会写相同的回调此操作,因为没有意义
    this.message[type].push(fn)
  }
  // 删除消息队列里边的内容
  off(type, fn) {
    // 删除消息分为删除整个事件及删除事件中的某个行为
    // 判断如果fn不存在,只有一个参数的情况
    if (!fn) {
      // 直接把这个事情取消掉
      delete this.message[type]
      return
    }
    // 判断是否真的订阅过类型为type的事件
    if (!this.message[type]) return
    this.message[type] = this.message[type].filter(item => item !== fn)
  }
  // 触发消息队列里边的内容
  emit(type) {
    // 判断是否有订阅过
    if (!this.message[type]) return
    this.message[type].forEach(item => {
    	item()
    })
  }
}
// 使用构造函数创建一个实例
const person1 = new Observer()
// 当你想拜托这个person1帮你观察一些内容的时候
// 告诉你一个行为,当这个行为出现的时候,告诉你干什么
person1.on('abc', handel1)
person1.on('abc', handel2)
person1.on('abc', handel3)
console.log(person1)

person1.off('abc', handel1)
console.log(person1)

person1.off('abc')
console.log(person1)

person1.emit('abc')

function handel1() { console.log('handel1') }
function handel2() { console.log('handel2') }
function handel3() { console.log('handel3') }

4.策略模式

核心:

一个问题匹配多个解决方案,不一定要用到哪一个,而且可能随时会继续增加多个方案

例子:购物车结算

=》有多种折扣计算方式
=》100减10
=》纯8折
// 接收两个参数:价格,折扣种类
function calcPrice(price, type) {
  if (type === '100-10') {
    price -= parseInt(price / 100)*10
  } else if (type === '80%') {
    price *= 0.8
  }
}
// 使用的时候
const res = calcPrice(320, '100-10')

以上代码可实现基本功能,但是问题是每次新增或删除一种策略模式就要手动去修改calcPrice函数源代码。

所以采取以下方案:

  • 把多种解决方案用闭包的形式保存起来,按照传递进来的价格和类型判断并计算结果返回
  • 对外留添加和删除的接口
const calcPrice = (function() {
  const sale = {
    '100-10': (price) => { return price -= parseInt(price / 100)*10 },
    '80%': (price) => { return price *= 0.8 }
  }
  function calcPrice(price, type) {
    if (!sale[type]) return // 判断对象里有没有折扣类型
    return sale[type](price) // 执行计算结果并返回
  }
  // 把函数当做一个对象,向里边添加一些成员
  calcPrice.add = function(type, fn) {
    if (sale[type]) return
    sale[type] = fn
  }
  calcPrice.del = function(type) {
    delete sale[type]
  }
  return calcPrice
})()

calcPrice.add('70%', (price) => { return price *= 0.8 })