JavaScript 常见设计模式解析

设计模式(Design pattern)是一套被反复使用、多数人知晓的、通过分类编目的、代码设计经验的总结。
使用设计模式是为了可重用代码、让代码更容易被他人理解、保证代码可靠性。
毫无疑问,设计模式于己于他人于系统都是多赢的;设计模式使代码编写真正工程化;设计模式是软件工程的基石脉络,如同大厦的结构同样。javascript

观察者模式 Observer Pattern

Observer模式也叫观察者模式、订阅/发布模式,是由GoF提出的23种软件设计模式的一种。
Observer模式是行为模式之一,它的做用是当一个对象的状态发生变化时,可以自动通知其余关联对象,自动刷新对象状态,或者说执行对应对象的方法。
这种设计模式能够大大下降程序模块之间的耦合度,便于更加灵活的扩展和维护。vue

观察者模式包含两种角色:java

  • 观察者(订阅者)
  • 被观察者(发布者)

核心思想:观察者只要订阅了被观察者的事件,那么当被观察者的状态改变时,被观察者会主动去通知观察者,而无需关心观察者获得事件后要去作什么,实际程序中多是执行订阅者的回调函数。es6

在各类框架中:vue中的$emit,Angular1.x.x中的$on$emit$broadcast,Angular2中的emit...都是最典型的例子。json

简单的例子:
假设你是一个班长,要去通知班里的某些人一些事情,与其一个一个的手动调用触发的方法(私下里一个一个通知),不如维护一个列表(建一个群),这个列表存有你想要调用的对象方法(想要通知的人);
以后每次通知事件的时候只要循环执行这个列表就行了(群发),而不用关心这个列表里有谁。设计模式

Javascript中实现一个例子:闭包

// 咱们向某dom文档订阅了点击事件,当点击发生时,他会执行咱们传入的callback
element.addEventListener(‘click’, callback2, false)
element.addEventListener(‘click’, callback2, false)复制代码

咱们用Javascript实现一个简单的播放器:app

// 一个播放器类
class Player {

  constructor() {
    // 初始化观察者列表
    this.watchers = {}

    // 模拟2秒后发布一个'play'事件
    setTimeout(() => {
      this._publish('play', true)
    }, 2000)

    // 模拟4秒后发布一个'pause'事件
    setTimeout(() => {
      this._publish('pause', true)
    }, 4000)
  }

  // 发布事件
  _publish(event, data) {
    if (this.watchers[event] && this.watchers[event].length) {
      this.watchers[event].forEach(callback => callback.bind(this)(data))
    }
  }

  // 订阅事件
  subscribe(event, callback) {
    this.watchers[event] = this.watchers[event] || []
    this.watchers[event].push(callback)
  }

  // 退订事件
  unsubscribe(event = null, callback = null) {
    // 若是传入指定事件函数,则仅退订此事件函数
    if (callback) {
      if (this.watchers[event] && this.watchers[event].length) {
        this.watchers[event].splice(this.watchers[event].findIndex(cb => Object.is(cb, callback)), 1)
      }

    // 若是仅传入事件名称,则退订此事件对应的全部的事件函数
    } else if (event) {
      this.watchers[event] = []

    // 若是未传入任何参数,则退订全部事件
    } else {
      this.watchers = {}
    }
  }
}

// 实例化播放器
const player = new Player()
console.log(player)

// 播放事件回调函数1
const onPlayerPlay1 = function(data) {
  console.log('1: Player is play, the `this` context is current player', this, data)
}

// 播放事件回调函数2
const onPlayerPlay2 = data => {
  console.log('2: Player is play', data)
}

// 暂停事件回调函数
const onPlayerPause = data => {
  console.log('Player is pause', data)
}

// 加载事件回调函数
const onPlayerLoaded = data => {
  console.log('Player is loaded', data)
}

// 可订阅多个不一样事件
player.subscribe('play', onPlayerPlay1)
player.subscribe('play', onPlayerPlay2)
player.subscribe('pause', onPlayerPause)
player.subscribe('loaded', onPlayerLoaded)

// 能够退订指定订阅事件
player.unsubscribe('play', onPlayerPlay2)
// 退订指定事件名称下的全部订阅事件
player.unsubscribe('play')
// 退订全部订阅事件
player.unsubscribe()

// 能够在外部手动发出事件(真实生产场景中,发布特性通常为类内部私有方法)
player._publish('loaded', true)复制代码

举个Vue中的例子吧:框架

// 事件发布者使用'vm.$emit、vm.$dispatch(vue1.0)、vm.$broadcast(vue1.0)发布事件
// 接受方使用$on方法或组件监听器订阅事件,传递一个回调函数
vm.$emit(event, […args]) // publish
vm.$on(event, callback) // subscribe
vm.$off([event, callback]) // unsubscribe

// 或者组件中监听事件
<component @event="callback" />

// 在Vue中不管是$on方法仍是组件监听事件最终都会转化为实例中的监听器复制代码

各框架中观察者模式的实现:
Angularjs(AngularJS 1.x.x)中的实现
一样,Vue中使用Object.defineProperty()实现对数据的双向绑定,在数据变动时,使用notify广播事件,最终一样执行对应属性所维护的Watchers列表进行回调。dom

中介者模式 Mediator Pattern

中介者在程序设计中很是常见,和观察者模式实现的功能很是类似。

形式上:不像观察者模式那样经过调用pub/sub的形式来实现,而是经过一个中介者统一来管理。

实质上:观察者模式经过维护一堆列表来管理对象间的多对多关系,中介者模式经过统一接口来维护一对多关系,且通讯者之间不须要知道彼此之间的关系,只须要约定好API便可。

简单说:就像一辆汽车的行驶系统,观察者模式中,你须要知道车内坐了几我的(维护观察者列表),当汽车发生到站、停车、开车...这些事件(被订阅者事件)时,你须要给这个列表中订阅对应事件的的每一个人进行通知;
在中介者模式中,你只须要在车内发出广播(到站啦、停车啦、上车啦...请文明乘车尊老爱幼啦...),而不用关心谁在车上,谁要上车谁要下车,他们本身根据广播作本身要作的事,哪怕他不听广播,听了也不作本身要作的事都无所谓。

中介者模式包含两种角色:

  • 中介者(事件发布者)
  • 通讯者

Javascript中实现一个例子:

// 汽车
class Bus {

  constructor() {

    // 初始化全部乘客
    this.passengers = {}
  }

  // 发布广播
  broadcast(passenger, message = passenger) {
    // 若是车上有乘客
    if (Object.keys(this.passengers).length) {

      // 若是是针对某个乘客发的,就单独给他听
      if (passenger.id && passenger.listen) {

        // 乘客他爱听不听
        if (this.passengers[passenger.id]) {
          this.passengers[passenger.id].listen(message)
        }

      // 否则就广播给全部乘客
      } else {
        Object.keys(this.passengers).forEach(passenger => {
          if (this.passengers[passenger].listen) {
            this.passengers[passenger].listen(message)
          }
        })
      }
    }
  }

  // 乘客上车
  aboard(passenger) {
    this.passengers[passenger.id] = passenger
  }

  // 乘客下车
  debus(passenger) {
    this.passengers[passenger.id] = null
    delete this.passengers[passenger.id]
    console.log(`乘客${passenger.id}下车`)
  }

  // 开车
  start() {
    this.broadcast({ type: 1, content: '前方无障碍,开车!Over'})
  }

  // 停车
  end() {
    this.broadcast({ type: 2, content: '老司机翻车,停车!Over'})
  }
}

// 乘客
class Passenger {

  constructor(id) {
    this.id = id
  }

  // 听广播
  listen(message) {
    console.log(`乘客${this.id}收到消息`, message)
    // 乘客发现停车了,因而本身下车
    if (Object.is(message.type, 2)) {
      this.debus()
    }
  }

  // 下车
  debus() {
    console.log(`我是乘客${this.id},我如今要下车`, bus)
    bus.debus(this)
  }
}

// 建立一辆汽车
const bus = new Bus()

// 建立两个乘客
const passenger1 = new Passenger(1)
const passenger2 = new Passenger(2)

// 俩乘客分别上车
bus.aboard(passenger1)
bus.aboard(passenger2)

// 2秒后开车
setTimeout(bus.start.bind(bus), 2000)

// 3秒时司机发现2号乘客没买票,2号乘客被驱逐下车
setTimeout(() => {
  bus.broadcast(passenger2, { type: 3, content: '同志你好,你没买票,请下车!' })
  bus.debus(passenger2)
}, 3000)

// 4秒后到站停车
setTimeout(bus.end.bind(bus), 3600)

// 6秒后再开车,车上已经没乘客了
setTimeout(bus.start.bind(bus), 6666)复制代码

上面例子中(固然,稍微扩展了点哈),Bus即为中介者对象,乘客为通讯者,乘客具备一些统一的方法API,Bus只管开车停车发广播,执行本身的事物,乘客在不断地接受广播,根据广播信息的类型和内容做出本身的判断,执行事务。

代理模式 Proxy Pattern

简单说就是:为对象提供一种代理以控制对这个对象的访问。

代理模式使得代理对象控制具体对象的引用。代理几乎能够是任何对象:文件,资源,内存中的对象,或者是一些难以复制的东西。

举个例子: 一个工厂制造商品(目标对象),你能够给这个工厂设置一个业务代理(代理对象),提供流水线管理,订单,运货,淘宝网店等多种行为能力(扩展属性)。
固然,里面还有最关键的一点就是,这个代理能把一些骗纸和忽悠都过滤掉,将最真实最直接的订单给工厂,让工厂可以专一于生产(控制访问)。

上面工厂的例子:

// 真实工厂
class Factory {

  constructor(count) {
    // 工厂默认有1000件产品
    this.productions = count || 1000
  }

  // 生产商品
  produce(count) {
    // 原则上低于5个工厂是不接单的
    this.productions += count
  }

  // 向外批发
  wholesale(count) {
    // 原则上低于10个工厂是不批发的
    this.productions -= count
  }
}

// 代理工厂
class ProxyFactory extends Factory {

  // 代理工厂默认第一次合做就从工厂拿100件库存
  constructor(count = 100) {
    super(count)
  }

  // 代理工厂向真实工厂下订单以前会作一些过滤
  produce(count) {
    if (count > 5) {
      super.produce(count)
    } else {
      console.log('低于5件不接单')
    }
  }

  wholesale(count) {
    if (count > 10) {
      super.wholesale(count)
    } else {
      console.log('低于10件不批发')
    }
  }

  taobao(count) {
      // ...
  }

  logistics() {
      // ...
  }
}

// 建立一个代理工厂
const proxyFactory = new ProxyFactory()

// 经过代理工厂生产4件商品,被拒绝
proxyFactory.produce(4)

// 经过代理工厂批发20件商品
proxyFactory.wholesale(20)

// 代理工厂的剩余商品 80
console.log(proxyFactory.productions)复制代码

ES6中的Proxy对象:

ES6中Proxy对象能够理解为:在目标对象以前架设一层“拦截”,外界对该对象的访问,都必须先经过这层拦截,所以提供了一种机制,能够对外界的访问进行过滤和改写。Proxy 这个词的原意是代理,用在这里表示由它来“代理”某些操做,能够译为"代理器"。

基本形式:

// 参数分别为目标对象和代理解析器
var proxy = new Proxy(target, handler)复制代码

无操做转发代理:

const target = {}
const p = new Proxy(target, {})
p.a = 3  // 被转发到代理的操做
console.log(target.a) // 3 操做已经被正确地转发至目标对象复制代码

使用错误拦截属性读取操做:

const handler = {
    get(target, property) {
        if (property in target) {
            return target[property]
        } else {
            throw new ReferenceError("Property \"" + property + "\" does not exist.")
        }
    }
}

const p = new Proxy({}, handler)
p.a = 1
p.b = undefined

console.log(p.a, p.b) // 1, undefined
console.log('c' in p, p.c) // Uncaught ReferenceError: Property "c" does not exist.复制代码

实现一个service客户端:

function createWebService(baseUrl) {
  return new Proxy({}, {
    get(target, propKey, receiver) {
      return () => httpGet(baseUrl+'/' + propKey)
    }
  })
}

const serviceA = createWebService('http://example.com/data-a')
const serviceB = createWebService('http://example.com/data-b')
const serviceC = createWebService('http://example.com/data-c')

serviceA.employees().then(json => {
  const employees = JSON.parse(json)
  // ···
})

serviceB...复制代码

单例模式 Singleton Pattern

简单说:保证一个类只有一个实例,并提供一个访问它的全局访问点(调用一个类,任什么时候候返回的都是同一个实例)。

实现方法:使用一个变量来标志当前是否已经为某个类建立过对象,若是建立了,则在下一次获取该类的实例时,直接返回以前建立的对象,不然就建立一个对象。

类/构造函数实例:

class Singleton {

  constructor(name) {
    this.name = name
    this.instance = null
  }

  getName() {
    alert(this.name)
  }

  static getInstance(name) {
    if (!this.instance) {
      this.instance = new Singleton(name)
    }
    return this.instance
  }
}

const instanceA = Singleton.getInstance('seven1')
const instanceB = Singleton.getInstance('seven2')

console.log(instanceA, instanceB)复制代码

闭包包装实例:

const SingletonP = (function() {
  let instance
  return class Singleton {

    constructor(name) {
      if (instance) {
        return instance
      } else {
        this.init(name)
        instance = this
        return this
      }
    }

    init(name) {
      this.name = name
      console.log('已初始化')
    }
  }
})()

const instanceA = new SingletonP('seven1')
const instanceB = new SingletonP('seven2')

console.log(instanceA, instanceB)复制代码

惰性包装实例:

const getSingle = function (fn) {
    let result
    return function() {
        return result || (result = fn.apply(this, arguments))
    }
}复制代码

工厂模式 Factory Pattern

与建立型模式相似,工厂模式建立对象(视为工厂里的产品)时无需指定建立对象的具体类。
工厂模式定义一个用于建立对象的接口,这个接口由子类决定实例化哪个类。该模式使一个类的实例化延迟到了子类。而子类能够重写接口方法以便建立的时候指定本身的对象类型。

简单说:假如咱们想在网页面里插入一些元素,而这些元素类型不固定,多是图片、连接、文本,根据工厂模式的定义,在工厂模式下,工厂函数只需接受咱们要建立的元素的类型,其余的工厂函数帮咱们处理。

上代码:

// 文本工厂
class Text {
    constructor(text) {
        this.text = text
    }
    insert(where) {
        const txt = document.createTextNode(this.text)
        where.appendChild(txt)
    }
}

// 连接工厂
class Link {
    constructor(url) {
        this.url = url
    }
    insert(where) {
        const link = document.createElement('a')
        link.href = this.url
        link.appendChild(document.createTextNode(this.url))
        where.appendChild(link)
    }
}

// 图片工厂
class Image {
    constructor(url) {
        this.url = url
    }
    insert(where) {
        const img = document.createElement('img')
        img.src = this.url
        where.appendChild(img)
    }
}

// DOM工厂
class DomFactory {

  constructor(type) {
    return new (this[type]())
  }

  // 各流水线
  link() { return Link }
  text() { return Text }
  image() { return Image }
}

// 建立工厂
const linkFactory = new DomFactory('link')
const textFactory = new DomFactory('text')

linkFactory.url = 'https://surmon.me'
linkFactory.insert(document.body)

textFactory.text = 'HI! I am surmon.'
textFactory.insert(document.body)复制代码

装饰者模式 Decorative Pattern

装饰者(decorator)模式可以在不改变对象自身的基础上,在程序运行期间给对像动态的添加职责(方法或属性)。与继承相比,装饰者是一种更轻便灵活的作法。

简单说:能够动态的给某个对象添加额外的职责,而不会影响从这个类中派生的其它对象。

实例:假设同事A在window.onload中指定了一些任务,这个函数由同事A维护,如何在对window.onload函数不进行任何修改的基础上,在window.onload函数执行最后执行本身的任务?

Show me the code:

// 同事A的任务
window.onload = () => {
    console.log('window loaded!')
}

// 装饰者
let _onload= window.onload || function () {}
window.onload = () => {
    _onload()
    console.log('本身的处理函数')
};复制代码

如何在全部函数执行先后分别执行指定函数:

// 新添加的函数在旧函数以前执行
Function.prototype.before = function (beforefn) {
    let _this = this
    return function () {
        beforefn.apply(this, arguments)
        return _this.apply(this, arguments)
    }
}

// 新添加的函数在旧函数以后执行
Function.prototype.after = function(afterfn) {
    let _this = this
    return function () {
        let ret = _this.apply(this, arguments)
        afterfn.apply(this, arguments)
        return ret
    }
}

// 使用
var func = function(param) {
    console.log(param)
}

func = func.before(function(param) {
    param.name = 'beforename'
})

func({ name: 'func' }) // { name: 'beforename' }复制代码

不污染Function原型的作法:

// 装饰器
const before = function(fn, before) {
    return function() {
        before.apply(this, arguments)
        return fn.apply(this, arguments)
    }
}

// 普通函数
function a() { console.log('a') }
function b() { console.log('b') }

// 使用装饰器执行函数
const c = before(a, b)

c() // b a复制代码

模拟传统语言的装饰者:

// 飞机
class Plane {

  constructor(name) {
    this.name = name
  }

  // 发射子弹
  fire() {
    console.log('发射普通子弹')
  }
}

// 武器增强版(装饰类)
class MissileDecorator {

  constructor(plane) {
    this.plane = plane
    this.plane.name = '高级飞机'
  }

  fire() {
    this.plane.fire()
    console.log('发射导弹')
  }
}

let plane = new Plane('普通飞机')
plane = new MissileDecorator(plane)
plane.fire()

// 发射普通子弹
// 发射导弹复制代码

使用ES7中的装饰器:

首先须要搞清楚ES6中Class语法糖的背后工做原理:

class Cat {
    say() {
        console.log("meow ~")
    }
}

// 实际上当咱们给一个类添加一个属性的时候,会调用到 Object.defineProperty 这个方法,它会接受三个参数:target 、name 和 descriptor ,上面的Class本质等同于:
function Cat() {}
Object.defineProperty(Cat.prototype, 'say', {
    value: function() { console.log("meow ~"); },
    enumerable: false,
    configurable: true,
    writable: true
})复制代码

ES7装饰器基本示例:

function isAnimal(target) {
    target.isAnimal = true
    return target
}

// 装饰器
@isAnimal
class Cat {
    // ...
}
console.log(Cat.isAnimal)    // true

// 上面装饰器代码基本等同于
Cat = isAnimal(function Cat() { ... })复制代码

做用于类属性的装饰器:

function readonly(target, name, descriptor) {
    discriptor.writable = false
    return discriptor
}

class Cat {
    @readonly
    say() {
        console.log("meow ~")
    }
}

var kitty = new Cat()
kitty.say = function() {
    console.log("woof !")
}
kitty.say()    // meow ~复制代码

在类的属性中定义装饰器的时候,参数有三个:targetnamedescriptor,上面说了,由于装饰器在做用于属性的时候,其实是经过Object.defineProperty来进行扩展和封装的。

因此在上面的这段代码中,装饰器实际的做用形式是这样的:

let descriptor = {
    value: function() {
        console.log("meow ~")
    },
    enumerable: false,
    configurable: true,
    writable: true
}
descriptor = readonly(Cat.prototype, 'say', descriptor) || descriptor
Object.defineProperty(Cat.prototype, 'say', descriptor)复制代码

这里也是 JS 里装饰器做用于类和做用于类的属性的不一样的地方。
当装饰器做用于类自己的时候,咱们操做的对象也是这个类自己,而当装饰器做用于类的某个具体的属性的时候,咱们操做的对象既不是类自己,也不是类的属性,而是它的描述符(descriptor),
而描述符里记录着咱们对这个属性的所有信息,因此,咱们能够对它自由的进行扩展和封装,最后达到的目的呢,就和以前说过的装饰器的做用是同样的。

也能够直接在 target 上进行扩展和封装,好比:

function fast(target, name, descriptor) {
    target.speed = 20
    let run = descriptor.value
    descriptor.value = function() {
        run()
        console.log(`speed ${this.speed}`)
    }
    return descriptor;
}

class Rabbit {
    @fast
    run() {
        console.log("running~")
    }
}

var bunny = new Rabbit()
bunny.run()
// running~
// speed 20
console.log(bunny.speed)   // 20复制代码

总结:装饰器容许你在类和方法定义的时候去注释或者修改它。装饰器是一个做用于函数的表达式,它接收三个参数targetnamedescriptor,而后可选性的返回被装饰以后的descriptor对象。

装饰者模式和代理模式的区别:

  1. 代理模式的目的是,当直接访问本体不方便或者不符合须要时,为这个本体提供一个代替者。本体定义了关键功能,而代理提供了或者拒绝对他的访问,或者是在访问本体以前作一些额外的事情。
  2. 装饰者模式的做用就是为对象动态的加入某些行为。

内容如有误差,期待指正修改。

原文地址:surmon.me/article/40

相关文章
相关标签/搜索