【设计模式】常用设计模式总结

JavaScript设计模式:https://juejin.cn/post/6844903503266054157
JavaScript设计模式:https://www.jianshu.com/p/90cebf0e529d
JavaScript设计模式整理:https://juejin.cn/post/6844903607452581896
JavaScript设计模式es6(23种):https://juejin.cn/post/6844904032826294286

以下内容来自:(大佬太强了!)

JavaScript设计模式整理:https://juejin.cn/post/6844903607452581896
JavaScript设计模式es6(23种):https://juejin.cn/post/6844904032826294286

设计模式原则

S – Single Responsibility Principle 单一职责原则

一个程序只做好一件事
如果功能过于复杂就拆分开,每个部分保持独立

O – OpenClosed Principle 开放/封闭原则

对扩展开放,对修改封闭
增加需求时,扩展新代码,而非修改已有代码

L – Liskov Substitution Principle 里氏替换原则

子类能覆盖父类
父类能出现的地方子类就能出现

I – Interface Segregation Principle 接口隔离原则

保持接口的单一独立
类似单一职责原则,这里更关注接口

D – Dependency Inversion Principle 依赖倒转原则

面向接口编程,依赖于抽象而不依赖于具体
使用方只关注接口而不关注具体类的实现

SO体现较多,举个栗子:(比如Promise)

单一职责原则:每个then中的逻辑只做好一件事
开放封闭原则(对扩展开放,对修改封闭):如果新增需求,扩展then

单例模式

单例模式两个条件

  • 确保只有一个实例
  • 可以全局访问

适用

适用于弹框的实现, 全局缓存

实现单例模式

const singleton = function(name) {
  this.name = name
  this.instance = null
}
singleton.prototype.getName = function() {
  console.log(this.name)
}
singleton.getInstance = function(name) {
  if (!this.instance) { // 关键语句
    this.instance = new singleton(name)
  }
  return this.instance
}
// test
const a = singleton.getInstance('a') // 通过 getInstance 来获取实例
const b = singleton.getInstance('b')
console.log(a === b)

JavaScript 中的单例模式

因为 JavaScript 是无类的语言, 而且 JS 中的全局对象符合单例模式两个条件。很多时候我们把全局对象当成单例模式来使用,

var obj = {}

弹框层的实践

实现弹框的一种做法是先创建好弹框, 然后使之隐藏, 这样子的话会浪费部分不必要的 DOM 开销, 我们可以在需要弹框的时候再进行创建, 同时结合单例模式实现只有一个实例, 从而节省部分 DOM 开销。下列为登入框部分代码:

const createLoginLayer = function() {
  const div = document.createElement('div')
  div.innerHTML = '登入浮框'
  div.style.display = 'none'
  document.body.appendChild(div)
  return div
}

使用单例模式将创建弹框代码解耦, 代码如下:

const getSingle = function(fn) {
  let result
  return function() {
    return result || (result = fn.apply(this, arguments))
  }
}
const createSingleLoginLayer = getSingle(createLoginLayer)
document.getElementById('loginBtn').onclick = function() {
  createSingleLoginLayer()
}

策略模式

策略模式

定义: 根据不同参数可以命中不同的策略

JavaScript 中的策略模式

观察如下获取年终奖的 demo, 根据不同的参数(level)获得不同策略方法(规则), 这是策略模式在 JS 比较经典的运用之一。

const strategy = {
  'S': function(salary) {
    return salary * 4
  },
  'A': function(salary) {
    return salary * 3
  },
  'B': function(salary) {
    return salary * 2
  }
}
const calculateBonus = function(level, salary) {
  return strategy[level](salary)
}
calculateBonus('A', 10000) // 30000

在函数是一等公民的 JS 中, 策略模式的使用常常隐藏在高阶函数中, 稍微变换下上述 demo 的形式如下, 可以发现我们平时已经在使用它了, 恭喜我们又掌握了一种设计模式。

const S = function(salary) {
  return salary * 4
}
const A = function(salary) {
  return salary * 3
}
const B = function(salary) {
  return salary * 2
}
const calculateBonus = function(func, salary) {
  return func(salary)
}
calculateBonus(A, 10000) // 30000

优点

  • 能减少大量的 if 语句
  • 复用性好

代理模式

代理模式

情景: 小明追女生 A

  • 非代理模式: 小明 =花=> 女生 A
  • 代理模式: 小明 =花=> 让女生 A 的好友 B 帮忙 =花=> 女生 A

代理模式的特点

  • 代理对象和本体对象具有一致的接口, 对使用者友好

代理模式的种类有很多, 在 JS 中最常用的为虚拟代理和缓存代理。

虚拟代理实现图片预加载

下面这段代码运用代理模式来实现图片预加载, 可以看到通过代理模式巧妙地将创建图片与预加载逻辑分离, 并且在未来如果不需要预加载, 只要改成请求本体代替请求代理对象就行。

const myImage = (function() {
  const imgNode = document.createElement('img')
  document.body.appendChild(imgNode)
  return {
    setSrc: function(src) {
      imgNode.src = src
    }
  }
})()
const proxyImage = (function() {
  const img = new Image()
  img.onload = function() { // http 图片加载完毕后才会执行
    myImage.setSrc(this.src)
  }
  return {
    setSrc: function(src) {
      myImage.setSrc('loading.jpg') // 本地 loading 图片
      img.src = src
    }
  }
})()
proxyImage.setSrc('http://loaded.jpg')
缓存代理实现乘积计算
const mult = function() {
  let a = 1
  for (let i = 0, l; l = arguments[i++];) {
    a = a * l
  }
  return a
}
const proxyMult = (function() {
  const cache = {}
  return function() {
    const tag = Array.prototype.join.call(arguments, ',')
    if (cache[tag]) {
      return cache[tag]
    }
    cache[tag] = mult.apply(this, arguments)
    return cache[tag]
  }
})()
proxyMult(1, 2, 3, 4) // 24

小 tip

在开发时候不要先去猜测是否需要使用代理模式, 如果发现直接使用某个对象不方便时, 再来优化不迟。

发布订阅模式

发布订阅模式

事件发布/订阅模式 (PubSub) 在异步编程中帮助我们完成更松的解耦, 甚至在 MVC、MVVC 的架构中以及设计模式中也少不了发布-订阅模式的参与。

优点: 在异步编程中实现更深的解耦

缺点: 如果过多的使用发布订阅模式, 会增加维护的难度

实现一个发布订阅模式

var Event = function() {
  this.obj = {}
}
Event.prototype.on = function(eventType, fn) {
  if (!this.obj[eventType]) {
    this.obj[eventType] = []
  }
  this.obj[eventType].push(fn)
}
Event.prototype.emit = function() {
  var eventType = Array.prototype.shift.call(arguments)
  var arr = this.obj[eventType]
  for (let i = 0; i < arr.length; i++) {
    arr[i].apply(arr[i], arguments)
  }
}
var ev = new Event()
ev.on('click', function(a) { // 订阅函数
  console.log(a) // 1
})
ev.emit('click', 1)          // 发布函数

订阅函数逻辑一定要优先于发布函数吗

考虑以下场景:

$.ajax('', () => {
  // 异步订阅函数逻辑
})
// 在其他地方执行发布函数, 此时并不能保证执行发布函数的时候, 订阅函数已经执行

我们需要实现这样的逻辑:

var ev = new Event()
ev.emit('click', 1)
ev.on('click', function(a) {
  console.log(a) // 1
})

目标明确后, 来着手实现它:

var Event = function() {
  this.obj = {}
  this.cacheList = []
}
Event.prototype.on = function(eventType, fn) {
  if (!this.obj[eventType]) {
    this.obj[eventType] = []
  }
  this.obj[eventType].push(fn)
  for (let i = 0; i < this.cacheList.length; i++) {
    this.cacheList[i]()
  }
}
Event.prototype.emit = function() {
  const arg = arguments
  const that = this
  function cache() {
    var eventType = Array.prototype.shift.call(arg)
    var arr = that.obj[eventType]
    for (let i = 0; i < arr.length; i++) {
      arr[i].apply(arr[i], arg)
    }
  }
  this.cacheList.push(cache)
}

以上代码实现思路就是把原本在 emit 里触发的函数存到 cacheList, 再转交到 on 中触发。从而实现了发布函数先于订阅函数执行。

观察者模式

观察者模式

应用场景:

  1. 场景一: 当观察的数据对象发生变化时, 自动调用相应函数。比如 vue 的双向绑定;
  2. 场景二: 每当调用对象里的某个方法时, 就会调用相应’访问’逻辑。比如给测试框架赋能的 spy 函数;

场景一: 双向绑定

Object.defineProperty

使用 Object.defineProperty(obj, props, descriptor) 实现观察者模式, 其也是 vue 双向绑定 的核心, 示例如下(当改变 obj 中的 value 的时候, 自动调用相应相关函数):

var obj = {
  data: { list: [] },
}
Object.defineProperty(obj, 'list', {
  get() {
    return this.data['list']
  },
  set(val) {
    console.log('值被更改了')
    this.data['list'] = val
  }
})
Proxy

Proxy/Reflect 是 ES6 引入的新特性, 也可以使用其完成观察者模式, 示例如下(效果同上):

var obj = {
  value: 0
}
var proxy = new Proxy(obj, {
  set: function(target, key, value, receiver) { // {value: 0}  "value"  1  Proxy {value: 0}
    console.log('调用相应函数')
    Reflect.set(target, key, value, receiver)
  }
})
proxy.value = 1 // 调用相应函数

场景二

下面来实现 sinon 框架的 spy 函数:

const sinon = {
  analyze: {},
  spy: function(obj, fnName) {
    const that = this
    const oldFn = Object.getOwnPropertyDescriptor(obj, fnName).value
    Object.defineProperty(obj, fnName, {
      value: function() {
        oldFn()
        if (that.analyze[fnName]) {
          that.analyze[fnName].count = ++that.analyze[fnName].count
        } else {
          that.analyze[fnName] = {}
          that.analyze[fnName].count = 1
        }
        console.log(`${fnName} 被调用了 ${that.analyze[fnName].count}`)
      }
    })
  }
}
const obj = {
  someFn: function() {
    console.log('my name is someFn')
  }
}
sinon.spy(obj, 'someFn')
obj.someFn()
// my name is someFn
// someFn 被调用了 1 次
obj.someFn()
// my name is someFn
// someFn 被调用了 2 次

vue 在 3.0 版本上使用 Proxy 重构的原因

首先罗列 Object.defineProperty() 的缺点:

  1. Object.defineProperty() 不会监测到数组引用不变的操作(比如 push/pop 等);
  2. Object.defineProperty() 只能监测到对象的属性的改变, 即如果有深度嵌套的对象则需要再次给之绑定 Object.defineProperty();

关于 Proxy 的优点

  1. 可以劫持数组的改变;
  2. defineProperty 是对属性的劫持, Proxy 是对对象的劫持;
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值