JavaScript 的一些常用设计模式

2019-08-19 admin

设计模式的定义:在面向对象软件设计过程中针对特定问题的简洁而优雅的解决方案

设计模式是前人解决某个特定场景下对而总结出来的一些解决方案。可能刚开始接触编程还没有什么经验的时候,会感觉设计模式没那么好理解,这个也很正常。有些简单的设计模式我们有时候用到,不过没意识到也是存在的。

学习设计模式,可以让我们在处理问题的时候提供更多更快的解决思路。

当然设计模式的应用也不是一时半会就会上手,很多情况下我们编写的业务逻辑都没用到设计模式或者本来就不需要特定的设计模式。

适配器模式

这个使我们常使用的设计模式,也算最简单的设计模式之一,好处在于可以保持原有接口的数据结构不变动。

适配器模式(Adapter Pattern)是作为两个不兼容的接口之间的桥梁。

例子

适配器模式很好理解,假设我们和后端定义了一个接口数据结构为(可以理解为旧接口):

[
  {
    "label": "选择一",
    "value": 0
  },
  {
    "label": "选择二",
    "value": 1
  }
]

但是后端后面因为其他原因,需要定义返回的结构为(可以理解为新接口):

[
  {
    "label": "选择一",
    "text": 0
  },
  {
    "label": "选择二",
    "text": 1
  }
]

然后我们前端的使用到后端接口有好几处,那么我可以把新的接口字段结构适配为老接口的,就不需要各处去修改字段,只要把源头的数据适配好就可以了。

当然上面的是非常简单的场景,也是经常用到的场景。或许你会认为后端处理不更好了,的确是这样更好,但是这个不是我们讨论的范围。

单例模式

单例模式,从字面意思也很好理解,就是实例化多次都只会有一个实例。

有些场景实例化一次,可以达到缓存效果,可以减少内存占用。还有些场景就是必须只能实例化一次,否则实例化多次会覆盖之前的实例,导致出现 bug(这种场景比较少见)。

例子

实现弹框的一种做法是先创建好弹框, 然后使之隐藏, 这样子的话会浪费部分不必要的 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) {
  const result
  return function() {
    return result || result = fn.apply(this, arguments)
  }
}
const createSingleLoginLayer = getSingle(createLoginLayer)

document.getElementById('loginBtn').onclick = function() {
  createSingleLoginLayer()
}

代理模式

代理模式的定义:为一个对象提供一个代用品或占位符,以便控制对它的访问。

代理对象拥有本体对象的一切功能的同时,可以拥有而外的功能。而且代理对象和本体对象具有一致的接口,对使用者友好。

虚拟代理

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

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')

缓存代理

在原有的功能上加上结果缓存功能,就属于缓存代理。

原先有个功能是实现字符串反转(reverseString),那么在不改变 reverseString 的现有逻辑,我们可以使用缓存代理模式实现性能的优化,当然也可以在值改变的时候去处理下其他逻辑,如 Vue computed 的用法。

function reverseString(str) {
  return str
    .split('')
    .reverse()
    .join('')
}
const reverseStringProxy = (function() {
  const cached = {}
  return function(str) {
    if (cached[str]) {
      return cached[str]
    }
    cached[str] = reverseString(str)
    return cached[str]
  }
})()

订阅发布模式

订阅发布使前端常用的数据通信方式、异步逻辑处理等等,如 React setState 和 Redux 就是订阅发布模式的。

但是要合理的使用订阅发布模式,否则会造成数据混乱,redux 的单向数据流思想可以避免数据流混乱的问题。

例子

class Event {
  constructor() {
    // 所有 eventType 监听器回调函数(数组)
    this.listeners = {}
  }
  /**
   * 订阅事件
   * @param {String} eventType 事件类型
   * @param {Function} listener 订阅后发布动作触发的回调函数,参数为发布的数据
   */
  on(eventType, listener) {
    if (!this.listeners[eventType]) {
      this.listeners[eventType] = []
    }
    this.listeners[eventType].push(listener)
  }
  /**
   * 发布事件
   * @param {String} eventType 事件类型
   * @param {Any} data 发布的内容
   */
  emit(eventType, data) {
    const callbacks = this.listeners[eventType]
    if (callbacks) {
      callbacks.forEach((c) => {
        c(data)
      })
    }
  }
}

const event = new Event()
event.on('open', (data) => {
  console.log(data)
})
event.emit('open', { open: true })

观察者模式

观察者模式定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个目标对象,当这个目标对象的状态发生变化时,会通知所有观察者对象,使它们能够自动更新。

Vue 的数据驱动就是使用观察者模式,mbox 也是使用观察者模式。

例子

模仿 Vue 数据驱动渲染模式(只是类似,简单的模仿)。

首先使用 setter 和 getter 监听到数据的变化:

const obj = {
  data: { description: '' },
}

Object.defineProperty(obj, 'description', {
  get() {
    return this.data.description
  },
  set(val) {
    this.data.description = val
  },
})

然后加上目标和观察者

class Subject {
  constructor() {
    this.observers = []
  }

  add(observer) {
    this.observers.push(observer)
  }

  notify(data) {
    this.observers.forEach((observer) => observer.update(data))
  }
}

class Observer {
  constructor(callback) {
    this.callback = callback
  }
  update(data) {
    this.callback && this.callback(data)
  }
}

// 创建观察者ob1
let ob1 = new Observer((text) => {
  document.querySelector('#dom-one').innerHTML(text)
})
// 创建观察者ob2
let ob2 = new Observer((text) => {
  document.querySelector('#dom-two').innerHTML(text)
})
// 创建目标sub
let sub = new Subject()
// 目标sub添加观察者ob1 (目标和观察者建立了依赖关系)
sub.add(ob1)
// 目标sub添加观察者ob2
sub.add(ob2)
// 目标sub触发事件(目标主动通知观察者)
sub.notify('这里改变了')

组合在一起是这样的

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <meta
      name="viewport"
      content="width=device-width,initial-scale=1,maximum-scale=1,viewport-fit=cover"
    />
    <title></title>
  </head>
  <body>
    <div id="app">
      <div id="dom-one">
        原来的值
      </div>
      <br />
      <div id="dom-two">
        原来的值
      </div>
      <br />
      <button id="btn">改变</button>
    </div>
    <script>
      class Subject {
        constructor() {
          this.observers = []
        }

        add(observer) {
          this.observers.push(observer)
        }

        notify() {
          this.observers.forEach((observer) => observer.update())
        }
      }

      class Observer {
        constructor(callback) {
          this.callback = callback
        }
        update() {
          this.callback && this.callback()
        }
      }

      const obj = {
        data: { description: '' },
      }

      // 创建观察者ob1
      const ob1 = new Observer(() => {
        console.log(document.querySelector('#dom-one'))
        document.querySelector('#dom-one').innerHTML = obj.description
      })
      // 创建观察者ob2
      const ob2 = new Observer(() => {
        document.querySelector('#dom-two').innerHTML = obj.description
      })
      // 创建目标sub
      const sub = new Subject()
      // 目标sub添加观察者ob1 (目标和观察者建立了依赖关系)
      sub.add(ob1)
      // 目标sub添加观察者ob2
      sub.add(ob2)

      Object.defineProperty(obj, 'description', {
        get() {
          return this.data.description
        },
        set(val) {
          this.data.description = val
          // 目标sub触发事件(目标主动通知观察者)
          sub.notify()
        },
      })
      btn.onclick = () => {
        obj.description = '改变了'
      }
    </script>
  </body>
</html>

装饰者模式

装饰器模式(Decorator Pattern)允许向一个现有的对象添加新的功能,同时又不改变其结构。

ES6/7 的decorator 语法提案,就是装饰者模式。

例子

class A {
  getContent() {
    return '第一行内容'
  }
  render() {
    document.body.innerHTML = this.getContent()
  }
}

function decoratorOne(cla) {
  const prevGetContent = cla.prototype.getContent
  cla.prototype.getContent = function() {
    return `
      第一行之前的内容
      <br/>
      ${prevGetContent()}
    `
  }
  return cla
}

function decoratorTwo(cla) {
  const prevGetContent = cla.prototype.getContent
  cla.prototype.getContent = function() {
    return `
      ${prevGetContent()}
      <br/>
      第二行内容
    `
  }
  return cla
}

const B = decoratorOne(A)
const C = decoratorTwo(B)
new C().render()

策略模式

在策略模式(Strategy Pattern)中,一个行为或其算法可以在运行时更改。

假设我们的绩效分为 A、B、C、D 这四个等级,四个等级的奖励是不一样的,一般我们的代码是这样实现:

/**
 * 获取年终奖
 * @param {String} performanceType 绩效类型,
 * @return {Object} 年终奖,包括奖金和奖品
 */
function getYearEndBonus(performanceType) {
  const yearEndBonus = {
    // 奖金
    bonus: '',
    // 奖品
    prize: '',
  }
  switch (performanceType) {
    case 'A': {
      yearEndBonus = {
        bonus: 50000,
        prize: 'mac pro',
      }
      break
    }
    case 'B': {
      yearEndBonus = {
        bonus: 40000,
        prize: 'mac air',
      }
      break
    }
    case 'C': {
      yearEndBonus = {
        bonus: 20000,
        prize: 'iphone xr',
      }
      break
    }
    case 'D': {
      yearEndBonus = {
        bonus: 5000,
        prize: 'ipad mini',
      }
      break
    }
  }
  return yearEndBonus
}

使用策略模式可以这样:

/**
 * 获取年终奖
 * @param {String} strategyFn 绩效策略函数
 * @return {Object} 年终奖,包括奖金和奖品
 */
function getYearEndBonus(strategyFn) {
  if (!strategyFn) {
    return {}
  }
  return strategyFn()
}

const bonusStrategy = {
  A() {
    return {
      bonus: 50000,
      prize: 'mac pro',
    }
  },
  B() {
    return {
      bonus: 40000,
      prize: 'mac air',
    }
  },
  C() {
    return {
      bonus: 20000,
      prize: 'iphone xr',
    }
  },
  D() {
    return {
      bonus: 10000,
      prize: 'ipad mini',
    }
  },
}

const performanceLevel = 'A'
getYearEndBonus(bonusStrategy[performanceLevel])

这里每个函数就是一个策略,修改一个其中一个策略,并不会影响其他的策略,都可以单独使用。当然这只是个简单的范例,只为了说明。

策略模式比较明显的特性就是可以减少 if 语句或者 switch 语句。

职责链模式

顾名思义,责任链模式(Chain of Responsibility Pattern)为请求创建了一个接收者对象的链。这种模式给予请求的类型,对请求的发送者和接收者进行解耦。这种类型的设计模式属于行为型模式。

在这种模式中,通常每个接收者都包含对另一个接收者的引用。如果一个对象不能处理该请求,那么它会把相同的请求传给下一个接收者,依此类推。

例子

function order(options) {
  return {
    next: (callback) => callback(options),
  }
}

function order500(options) {
  const { orderType, pay } = options
  if (orderType === 1 && pay === true) {
    console.log('500 元定金预购, 得到 100 元优惠券')
    return {
      next: () => {},
    }
  } else {
    return {
      next: (callback) => callback(options),
    }
  }
}

function order200(options) {
  const { orderType, pay } = options
  if (orderType === 2 && pay === true) {
    console.log('200 元定金预购, 得到 50 元优惠券')
    return {
      next: () => {},
    }
  } else {
    return {
      next: (callback) => callback(options),
    }
  }
}

function orderCommon(options) {
  const { orderType, stock } = options
  if (orderType === 3 && stock > 0) {
    console.log('普通购买, 无优惠券')
    return {}
  } else {
    console.log('库存不够, 无法购买')
  }
}

order({
  orderType: 3,
  pay: true,
  stock: 500,
})
  .next(order500)
  .next(order200)
  .next(orderCommon)
// 打印出 “普通购买, 无优惠券”

上面的代码,对 order 相关的进行了解耦,order500order200orderCommon 等都是可以单独调用的。

参考文章

[转载]原文链接:https://segmentfault.com/a/1190000020115722

本站文章除注明转载外,均为本站原创或编译。欢迎任何形式的转载,但请务必注明出处。

转载请注明:文章转载自 JavaScript中文网 [https://www.javascriptcn.com]

本文地址:https://www.javascriptcn.com/read-72919.html

文章标题:JavaScript 的一些常用设计模式

相关文章
vue 数组遍历方法forEach和map的原理解析和实际应用
一、前言 forEach和map是数组的两个方法,作用都是遍历数组。在vue项目的处理数据中经常会用到,这里介绍一下两者的区别和具体用法示例。 二、代码 1. 相同点 都是数组的方法 都用来遍历数组 两个函数都有4个参数:匿名函数中可传3...
2018-11-15
html5+JavaScript教程-微信打飞机小游戏源码
js &#x2F;&#x2F; JavaScript Document var c = document.getElementById(&quot;dotu&quot;); var cxt = c.getContext(&quot;2d&q...
2015-11-12
JavaScript编辑器推荐
主流编辑器有SublimeText,Notepad++,webstorm等,是使用最广泛的编辑器,但也有一些JavaScript编辑器提供有着各自的特性和功能,适应不同人的需求,以下是几款优秀的编辑器,相信你一定能找到自己喜欢的。 1. W...
2015-11-12
js性能优化 如何更快速加载你的JavaScript页面
确保代码尽量简洁 不要什么都依赖JavaScript。不要编写重复性的脚本。要把JavaScript当作糖果工具,只是起到美化作用。别给你的网站添加大量的JavaScript代码。只有必要的时候用一下。只有确实能改善用户体验的时候用一下。 ...
2015-11-12
10个强大的纯CSS3动画案例分享
我们的网页外观主要由CSS控制,编写CSS代码可以任意改变我们的网页布局以及网页内容的样式。CSS3的出现,更是可以让网页增添了不少动画元素,让我们的网页变得更加生动有趣,并且更易于交互。本文分享了10个非常炫酷的CSS3动画案例,希望大家...
2015-11-16
2015年JavaScript或“亲库而远框架”
2014年过去了,作为一个JavaScript开发者很难满怀信心的去“挽回”一个特定的库或技术,即便是强大的Angular,似乎也因为最近的一些事情而动摇。 2014年10月的ng-europe会议上,Angular开发者团队透露了一个关于...
2015-11-12
v-charts | 饿了么团队开源的基于 Vue 和 ECharts 的图表工具
在使用echarts生成图表时,经常需要做繁琐的数据类型转化、修改复杂的配置项,v-charts的出现正是为了解决这个 痛点。基于Vue2.0和echarts封装的v-charts图表组件,只需要统一提供一种对前后端都友好的数据格式 设置简...
2018-05-24
JavaScript实现PC手机端和嵌入式滑动拼图验证码三种效果
PC和手机端网站滑动拼图验证码效果源码,同时包涵了弹出式Demo,使用ajax形式提交二次验证码所需的验证结果值,嵌入式Demo,使用表单形式提交二次验证所需的验证结果值,移动端手动实现弹出式Demo三种效果 首先要确认前端使用页面,比如...
2017-03-17
JavaScript常用特效chm下载
下载地址:JavaScript常用特效chm下载 对了,如果打开空白,在手册上右键属性解除锁定即可。 ...
2015-11-12
从2014年的发展来展望JS的未来将会如何
&lt;font face=&quot;寰�杞�闆呴粦, Arial, sans-serif &quot;&gt;2014骞达紝杞�浠惰�屼笟鍙戝睍杩呴€燂紝鍚勭�嶈��瑷€灞傚嚭涓嶇┓锛屼互婊¤冻鐢ㄦ埛涓嶆柇鍙樺寲鐨勯渶姹傘€傝繖浜涜��...
2015-11-12
回到顶部