JavaScript中的设计模式

351 阅读18分钟

最近阅读了《JavaScript设计模式与开发实践》,感受颇深,好的设计可以优化性能,大大提高代码的可阅读性和可维护性,但是想要培养出这样的抽象思维,不是一朝一夕的,所以记录本片文章,方便自己日后回忆理解,也希望对你有所帮助。

原型模式

在原型编程的思想中,类并不是必需的,对象未必需要从类中创建而来,一个对象是通过克隆另外一个对象所得到的。既然原型模式是通过克隆来创建对象的,那么很自然地会想到,如果需要一个跟某个对象一模一样的对象,就可以使用原型模式。

想要实现克隆,在ES5及以后版本,我们可以使用Object.create(),这里我们可以看一个通用的克隆方法:

Object.create = Object.create || function (obj) {
  var F = function () {}
  F.prototype = obj
  return new F()
}

原型编程范型包括以下基本规则:

  • 所有的数据都是对象。
  • 要得到一个对象,不是通过实例化类,而是找到一个对象作为原型并克隆它。
  • 对象会记住它的原型。
  • 如果对象无法响应某个请求,它会把这个请求委托给它自己的原型。

想要理解上面这几个规则,我们需要先了解一下JavaScript的原型链。

原型和原型链

根元素

我们知道JavaScript是基于原型编程设计的,与其他通过类来创建对象的语言不同,JavaScript的对象都是通过克隆根元素创建的,这个根元素就是 Object.prototypeObject.prototype 没有原型 ,他是所有其他对象的原型。

Object.prototype.__proto__ === null // ture

那么克隆是怎么发生的呢?当我们显式调用 var obj1 = new Object()var obj2 = {} 时,引擎内部会自己从 Object.prototype 上面克隆一个对象出来。

constructor

在JavaScript中,我们经常会通过一个function来新建一个对象,就像下面:

function Person (name) {
  this.name = name
}
var person = new Person ('Bob')

这里的 Person 并不是类,而是函数构造器,JavaScript 的函数既可以作为普通函数被调用, 也可以作为构造器被调用。当使用 new 运算符来调用函数时,此时的函数就是一个构造器。

通过原型的 constructor 属性,我们可以找到原型关联的构造函数。

Person === Person.prototype.constructor // true

prototype

每个函数都有一个 prototype 属性,这个 prototype 属性实际上就指向了调用该构造函数而创建的对象实例的原型。在原型上做的操作会反映到所以创建出来的对象实例。

function Person () {}
Person.name = 'Bob'
var person1 = new Person ()
var person2 = new Person ()
console.log(person1.name) // Bob
console.log(person2.name) // Bob

proto

通过构造函数创建的对象,可以通过 __proto__ 属性找到原型。

function Person () {}
var person = new Person ()
person._proto_ === Person.prototype // true

原型链

根据上面我们了解过的 constructorprototype__proto__ 属性,我们可以用一张图来表示他们之间的关系:

左边的部分就是原型链了,那么原型链有什么作用呢?当一个对象无法响应某个请求的时候,它会顺着原型链把请求传递下去,直到遇到一个可以处理该请求的对象为止。我们来看一个例子:

var obj = { name: 'Bob' }
var A = function () {}
A.prototype = obj
var B = function () {}
B.prototype = new A()
var b = new B()
console.log(b.name) // 'Bob'

在这个例子里,b对象本身并没有name值,顺着原型链,最终找到了在A.prototype上的name属性。

this 和 call, apply, bind

由于JS的设计模式里,将会频繁接触到 thiscallapplybind,所以我们先来了解一下他们。

this

关于 this 的指向问题,一直是前端工程师的必备基本功,想要了解更为详细的 this 解读,推荐大家看一看冴羽老师的这篇文章:从ECMAScript规范解读this

这里,我们简单对this的指向做个总结,this 的指向大致可以分为以下 4 种:

  • 作为对象的方法调用。
  • 作为普通函数调用。
  • 构造器调用。
  • Function.prototype.call 或 Function.prototype.apply 调用。

作为对象的方法调用

指向这个对象。

var obj = {
  name: 'Bob',
  getName: function () {
    console.log(this.name)
  }
}
obj.getName() // 'Bob'

作为普通函数调用

指向调用它的环境。

window.name = 'Bob'
var getName = function () {
  console.log(this.name)
}
getName() // 'Bob'

构造器调用

指向构造器返回的对象。

// 例1
var Person = function () {
  this.name = 'Bob'
}
var person = new Person ()
console.log(person.name) // 'Bob'

// 例2
var Person = function () {
  this.name = 'Bob'
  return {
    name: 'John'
  }
}
var person = new Person ()
console.log(person.name) // 'John'

Function.prototype.call 或 Function.prototype.apply 调用

this指向改变。

var obj1 = {
  name: 'Bob', 
  getName: function () {
    return this.name
  }
}
var obj2 = { 
   name: 'John'
}
console.log(obj1.getName()) // Bob 
console.log(obj1.getName.call(obj2))   // John

call, apply

Function.prototype.callFunction.prototype.apply 都是常用来改变this指向的方法。它们的作用一模一样,区别仅在于传入参数形式的不同。在上一节里我们已经大致了解了他们的使用,这里我们通过自己写一个模拟的 callapply 来加深理解。

apply

Function.prototype.apply = function (context, arr) {
  var context = context || window // 解决传入null作为this指向
  context.fn = this // 将需要绑定的方法作为目标对象的属性

  var res
  if (!arr) {
    res = context.fn ()
  } else {
    var args = []
    for (var i = 0 i < arr.length i++) {
      args.push('arr[' + i + ']')
    }
    res = eval('context.fn (' + args +')') // 执行方法
  }
  
  delete context.fn // 删除方法
  return res
}

// 使用
var person = {
  name: 'Bob'
}
var getInfo = function (age, gender) {
  this.age = age
  this.gender = gender
  console.log('name', this.name)
  console.log('age', this.age)
  console.log('gender', this.gender)
}
getInfo.apply(person, [13, 'male'])

call

Function.prototype.call = function (context) {
  var context = context || window // 解决传入null作为this指向
  context.fn = this // 将需要绑定的方法作为目标对象的属性

  // 解决参数长度不固定
  var args = []
  for (var i = 0 i < arguments.length i++) {
    args.push('arguments[' + i + ']') // arguments是个特殊的类数组对象
  }
  var res = eval('context.fn (' + args +')') // 执行方法
  delete context.fn // 删除方法
  return res
}

// 使用
var person = {
  name: 'Bob'
}
var getInfo = function (age, gender) {
  this.age = age
  this.gender = gender
  console.log('name', this.name)
  console.log('age', this.age)
  console.log('gender', this.gender)
}
getInfo.call(person, 13, 'male')

bind

Function.prototype.bind = function () {
  var _this = this
  context = [].shift.call(arguments), // 需要绑定的 this 上下文
  // 这个时候的arguments是指bind时传入的参数
  args = [].slice.call(arguments)
  return function () {
    // 这个时候的arguments是指bind返回的函数传入的参数
    var bindArgs = [].slice.call(arguments)
    return _this.apply(context, [].concat.call(args, bindArgs))
  }
}

// 使用
var person = {
  name: 'Bob'
}
var getInfo = function (age, gender) {
  this.age = age
  this.gender = gender
  console.log('name', this.name)
  console.log('age', this.age)
  console.log('gender', this.gender)
}
var bindGet = getInfo.bind(person, 13)
bindGet('male')

高阶函数

高阶函数是指至少满足下列条件之一的函数:

  • 函数可以作为参数被传递。
  • 函数可以作为返回值输出。

对于这两条特性,大家应该并不陌生,所以我们这里重点讲讲高阶函数的应用。

实现AOP

AOP(面向切面编程)的主要作用是把一些跟核心业务逻辑模块无关的功能抽离出来,再通过“动态织入”的方式掺入业务逻辑模块中。

Function.prototype.before = function (fn) {
  var _self = this
  return function () {
    fn.apply(this, arguments)
    return _self.apply(this, arguments)
  }
}

Function.prototype.after = function (fn) {
  var _self = this
  return function () {
    var ret = _self.apply(this, arguments)
    return fn.apply(this, arguments)
  }
}

currying

currying 又称部分求值。一个 currying 的函数首先会接受一些参数,接受了这些参数之后,该函数并不会立即求值,而是继续返回另外一个函数,刚才传入的参数在函数形成的闭包中被保存起来。待到函数被真正需要求值的时候,之前传入的所有参数都会被一次性用于求值。

这里我们来写一个通用的封装currying函数:

var currying = function (fn) {
  var args = []
  return function () {
    if (arguments.length === 0) {
      return fn.apply(this, args)
    } else {
      [].push.apply(args, arguments)
      return arguments.callee
    }
  }
}

在这个通用函数里,我们将每次传入的参数都存储在args中,通过闭包保存了起来,现在我们来看看怎么使用这个currying函数:

var count = function () {
  var score = 0
  for(var i = 0 i < arguments.length i++) {
    score += arguments[i]
  }
  return score
}

var count = currying(count)
count(1)
count(2)
console.log(count()) // 3
count(3)
console.log(count()) // 6

uncurrying

uncurrying的目的,是让一个对象可以使用原本不属于它的方法,同样,我们也来看一个通用封装函数:

Function.prototype.uncurrying = function () {
  var _self = this
  return function () {
    var obj = Array.prototype.shift.call(arguments)
    return _self.apply(obj, arguments)
  }
}

这个通用函数实现了对类数组进行数组操作,具体的使用是这样的:

var push = Array.prototype.push.uncurrying()
(function () {
  push(arguments, 4)
  console.log(arguments)
})(1, 2, 3)

var obj = {
  "length": 3,
  "0": 1,
  "1": 2,
  "2": 3,
}
push(obj, 4)
console.log(obj) // {0: 1, 1: 2, 2: 3, 3: 4, length: 4}

函数节流和防抖

函数节流和防抖都是为了防止一些函数被高频率调用的一种性能优化,他们的解决思路略有不同:

  • 函数节流:规定在一个单位时间内,只能触发一次函数。如果这个单位时间内触发多次函数,只有一次生效。
  • 函数防抖:在事件被触发n秒后再执行回调,如果在这n秒内又被触发,则重新计时。

下面我们来看看具体的实现过程。

函数节流

function throttle(fn, delay) {
  let last, timer
  return function () {
    let args = arguments
    let _self = this
    let now = + new Date()
    if (last && now < last + delay) {
      clearTimeout(timer)
      timer = setTimeout(() =>{
        last = now
        fn.apply(_self, args)
      }, delay)
    } else {
      last = now
      fn.apply(_self, args)
    }
  }
}

let printInfo = function () {
  console.log('Biu biu:', arguments)
} 
let throttleAjax = throttle(printInfo, 1000)
var input = document.getElementById('throttle')
input.addEventListener('keyup', function (e) {
  throttleAjax(e.target.value)
})

所以函数节流就是在固定时间内,只会执行一次函数。类似fps游戏的射速,就算一直按着鼠标射击,也只会在规定射速内射出子弹。

函数防抖

function debounce(fn, delay) {
  return function () {
    let _self = this
    let args = arguments
    clearTimeout(fn.id)
    fn.id = setTimeout(function () {
      fn.apply(_self, args)
    }, delay)
  }
}

let printInfo = function () {
  console.log('Biu biu:', arguments)
} 
let debounceAjax = debounce(printInfo, 1000)
var input = document.getElementById('debounce')
input.addEventListener('keyup', function (e) {
  debounceAjax(e.target.value)
})

函数防抖类似于法师发技能的时候要读条,技能读条没完再按技能就会重新读条。

两者的使用场景

函数节流:

  • 鼠标不断点击触发,mousedown (单位时间内只触发一次)。
  • 监听滚动事件,比如是否滑到底部自动加载更多,用throttle来判断。

函数防抖:

  • search搜索联想,用户在不断输入值时,用防抖来节约请求资源。
  • window触发resize的时候,不断的调整浏览器窗口大小会不断的触发这个事件,用防抖来让其只触发一次。

设计模式

单例模式

  • 定义:保证一个类仅有一个实例,并提供一个访问它的全局访问点。
  • 应用场景:页面中的弹出框组件。

简单的单例模式实现:

var Singleton = function (name) {
  this.name = name
  this.instance = null
}

Singleton.prototype.getName = function () {
  return this.name
}

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

var a = Singleton.getInstance('sven1') 

但是这样的单例模式,需要调用getInstance创建实例,不够透明,通常我们使用会通过new创建,所以我们要对这个单例进行优化。

代理的单例模式

为了符合“单一职责原则”,我们把创建单例实例分为两个函数:

  1. 用来约束实例唯一性的函数
  2. 用来创建初始化实例的函数
var CreateDiv = function (html) {
  this.html = html
  this.init()
}

CreateDiv.prototype.init = function () {
  let div = document.createElement('div')
  div.innerHTML = this.html
  document.body.appendChild(div)
}

var proxySignleton = function () {
  let instance = null
  return function (html) {
    if (!instance) {
      return new CreateDiv(html)
    }
    return instance
  }
}

var a = new proxySignleton ('sven1')  // <div>sven1</div>
var b = new proxySignleton ('sven2')  // <div>sven2</div>

惰性单例

惰性单例的惰性就体现在当我们需要的时候才创建单例的实例,这样可以优化性能。

var CreateDiv = function (html) {
  let div = document.createElement('div')
  div.innerHTML = html
  div.display = 'none'
  document.body.appendChild(div)
  return div
}

var proxySignleton = function (fn) {
  let instance = null
  return function () {
    return instance || (instance =  fn.apply(this, arguments))
  }
}

var createSingle = proxySignleton (CreateDiv)
document.getElementById('btn').onclick = function () {
  var div = createSingle()
  div.display = 'block'
}

这种方法也可以用来控制只绑定一次事件监听:

var eventlisten = function (id) {
  document.getElementById(id).onclick = function () {
    // do something
  }
}

var getSingle = function (fn) {
  let instance = null
  return function () {
    return instance || (instance =  fn.apply(this, arguments))
  }
}

var bindEvent = getSingle(eventlisten)

bindEvent('btn')

策略模式

  • 定义:定义一系列的算法,把它们一个个封装起来,并且使它们可以相互替换。
  • 应用场景:一个压缩文件的程序,既可以选择zip算法,也可以选择gzip算法。

一个基于策略模式的程序至少由两部分组成:

  1. 策略类:封装了具体的算法,并负责具体的计算过程。
  2. 环境类:接受客户的请求,随后把请求委托给某一个策略类。

策略模式实现:

var perfomanceS = {}
perfomanceS.prototype.calculate = function (salary) {
  return salary * 4
}

var perfomanceA = {}
perfomanceA.prototype.calculate = function (salary) {
  return salary * 3
}

var perfomanceB = {}
perfomanceB.prototype.calculate = function (salary) {
  return salary * 2
}

var Bonus = function () {
  this.salary = null
  this.strategy = null
}

Bonus.prototype.setSalary = function (salary) {
  this.salary = salary
}

Bonus.prototype.setStrategy = function (stategy) {
  this.strategy = strategy
}

Bonus.prototype.getBonus = function () {
  return this.strategy.calculate(this.salary)
}

let a = new Bonus()
a.setSalary(1000)
a.setStrategy(new perfomanceS)
console.log(a.getBonus) // 4000

a.setStrategy(new perfomanceA)
console.log(a.getBonus) // 3000

JS版策略模式:

var strategy = {
  'S': function (salary) {
    return salary * 4
  },
  'A': function (salary) {
    return salary * 3
  },
  'B': function (salary) {
    return salary * 2
  }
}

var calculateBonus = function (salary, level) {
  return strategy[leval](salary)
}

console.log(calculateBonus(1000, 'S')) // 4000
console.log(calculateBonus(1000, 'A')) // 3000

优点:

  • 策略模式利用组合、委托和多态等技术和思想,可以有效地避免多重条件选择语句。
  • 策略模式提供了对开放—封闭原则的完美支持,将算法封装在独立的strategy 中,使得它们易于切换,易于理解,易于扩展。
  • 策略模式中的算法也可以复用在系统的其他地方,从而避免许多重复的复制粘贴工作。
  • 在策略模式中利用组合和委托来让Context 拥有执行算法的能力,这也是继承的一种更轻便的替代方案。

缺点:

  • 使用策略模式会在程序中增加许多策略类或者策略对象。
  • 必须了解所有的strategy,必须了解各个strategy 之间的不同点。

代理模式

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

两种代理模式:

  1. 保护模式:用于控制不同权限的对象对目标对象的访问。
  2. 虚拟模式:把一些开销很大的对象,延迟到真正需要它的时候才去创建。

使用虚拟代理实现图片预加载:

var myImage = (function () {
  var imagenode = document.createElement('img')
  document.body.appendChild(imagenode)

  return {
    setSrc: function (src) {
      imagenode.src = src
    }
  }
})()

var proxyImage = (function () {
  var img = new Image()
  img.onload = function () {
    myImage.setSrc(this.src)
  }

  return {
    setSrc: function (src) {
      myImage.setSrc('file://someLocalPic')
      this.src = src
    }s
  }
})()

proxyImage.setSrc('http://someInternetPic')

引入代理模式,是为了符合面向对象设计的原则——单一职责原则。

单一职责原则:单一职责原则指的是,就一个类(通常也包括对象和函数等)而言,应该仅有一个引起它变化的原因。如果一个对象承担了多项职责,就意味着这个对象将变得巨大,引起它变化的原因可能会有多个。面向对象设计鼓励将行为分布到细粒度的对象之中,如果一个对象承担的职责过多,等于把这些职责耦合到了一起,这种耦合会导致脆弱和低内聚的设计。当变化发生时,设计可能会遭到意外的破坏。

下面看几个代理的应用。

代理合并请求

  • 目的:减少频繁请求响应。
var getAsyncFile = function (url) {
  // get url
}

var proxyGet = (function () {
  var cache = []
  var timer = null

  return function (url) {
    cache.push(url)

    timer = setTimeout({
      cache.map(url => {
        getAsyncFile(url)
      })
      clearTimeout(timer)
      timer = null
      cache = []
    }, 2000)
  }
})()

var checkbox = document.getElementsByTagName('input') 

for (var i = 0, c c = checkbox[i++]) {
  c.onclick = function () {
    if (this.checked === true)  {
      proxyGet(this.id) 
    }
  }
}

惰性加载中的代理

  • 目的:当触发某一条件时才执行。
var miniConsole = (function () {
  var cache = []
  var handler = function (ev) {
    if (ev.keyCode === 113) {
      var script = document.createElement('script') 
      script.onload = function () {
        for (var i = 0, fn fn = cache[ i++ ]) {
          fn ()
        }
      }
      script.src = 'miniConsole.js'
      document.getElementsByTagName('head')[0].appendChild(script)
      document.body.removeEventListener('keydown', handler) // 只加载一次miniConsole.js
    }
  }
  document.body.addEventListener('keydown', handler, false)
  return {
    log: function () {
      var args = arguments
      cache.push(function () {
        return miniConsole.log.apply(miniConsole, args)
      })
    }
  }
})()

miniConsole.log(11) 

缓存代理

  • 目的:为一些开销大的运算结果提供暂时的存储,在下次运算时,如果传递进来的参数跟之前一致,则可以直接返回前面存储的运算。
var plus = function () {
  var a = 0
  for (var i = 0 i < arguments.length i++) {
    a += arguments[i]
  }
  return a
}

var minus = function () {
  var a = 0
  for (var i = 0 i < arguments.length i++) {
    a -= arguments[i]
  }
  return a
}

var proxyCal = function (fn) {
  var cache = {}
  return function () {
    var arg = Array.prototype.join.call(arguments, ',')
    if (arg in cache) {
      return cache[arg]
    } else {
      return cache[arg] = fn.apply(this, arguments)
    }
  }
}

var createPlus = proxyCal(plus)
var createMinus = proxyCal(minus)

createPlus(1, 2, 3) // 第一次计算
createPlus(1, 2, 3) // 缓存
createMinus(1, 2, 3) // 第一次计算
createMinus(1, 2, 3) // 缓存

迭代器模式

  • 定义:指提供一种方法顺序访问一个聚合对象中的各个元素,而又不需要暴露该对象的内部表示。

很多语言都内置了迭代器:

Array.prototype.map = (callback) => {
  for (var i = 0 i < this.length i++) {
    callback.call(this, this[i])
  }
  return this
}

发布—订阅模式

  • 定义:又叫观察者模式,它定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知。
var Event = (function () {
  var clientList = {}
  
  var listen = function (key, fn) {
    if (!clientList[key]) {
      clientList[key] = []
    }
    clientList[key].push(fn)
  }

  var trigger = function () {
    let key = Array.prototype.shift.call(arguments)
    let fns = clientList[key]
    if (fns && fns.length > 0) {
      fns.map(fn => {
        fn.apply(this, arguments)
      })
    }
  }

  var remove = function (key, fn) {
    let fns = clientList[key]
    if (!fns) {
      return false
    }
    if (!fn) {
      fns = []
    } else {
      let idx = fns.findIndex(fn)
      fns.splice(idx, 1)
    }
  }

  return {
    listen,
    trigger,
    remove
  }
})

命令模式

  • 应用场景:有时候需要向某些对象发送请求,但是并不知道请求的接收者是谁,也不知道被请求的操作是什么。
var Ryu = {
  'attack': function () {
    console.log('攻击')
  },
  'jump': function () {
    console.log('跳跃')
  }
}

// 创建命令
var makeCommand = function (reciver, state) {
  return function () {
    reciver[state]()
  }
}

var commands = {
  "119": "jump", // W
  "115": "crouch", // S
  "97": "defense", // A
  "100": "attack" // D
}

var commandStack = []

document.onKeypress = function (e) {
  var key = e.keyCode
  var command = makeCommand(Ryu, commands[key])

  if (command) {
    command() // 执行命令
    commandStack.push(command)
  }
}

document.getElementById('replay').onclick = function () { // 点击播放录像
  var command
  while (command = commandStack.shift()) { // 从堆栈里依次取出命令并执行
    command()
  }
}

组合模式

  • 组合模式将对象组合成树形结构,以表示“部分——整体”的层次结构。
  • 通过对象的多态性表现,使得用户对单个对象和组合对象的使用具有一致性。
var Folder = function (name) {
  this.name = name
  this.parent = null
  this.files = []
}

Folder.prototype.add = function (file) {
  file.parent = this
  this.files.push(file)
}

Folder.prototype.scan = function () {
  for (var i = 0, file, files = this.files file = files[i++]) {
    file.scan ()
  }
}

Folder.prototype.remove = function () {
  if (!this.parent) {
    return false
  } else {
    for (var files = this.parent.files, l = files.length - 1 l >= 0 l--) {
      var file = files[l]
      if (file === this) {
        files.splice(l, 1)
      }
    }
  }
}

var File = function (name) {
  this.parent = null
  this.name = name
}

File.prototype.add = function () {
  throw new Error('不支持该方法')
}

File.prototype.scan = function () {
  console.log(`scan file ${this.name}`)
}

File.prototype.remove = function () {
  if (!this.parent) {
    return false
  } else {
    for (var files = this.parent.files, l = files.length - 1 l >= 0 l--) {
      var file = files[l]
      if (file === this) {
        files.splice(l, 1)
      }
    }
  }
}

var folder = new Folder('学习资料')
var folder1 = new Folder('JavaScript')
var folder2 = new Folder ('jQuery')
var file1 = new File('JavaScript 设计模式与开发实践')
var file2 = new File('精通jQuery')
var file3 = new File('重构与模式')
folder1.add(file1)
folder2.add(file2)
folder.add(folder1)
folder.add(folder2)
folder.add(file3)

folder.scan ()
folder1.remove()

使用注意点:

  1. 组合模式不是父子关系:组合模式是一种HAS-A(聚合)的关系,而不是IS-A。
  2. 对叶对象操作的一致性:要求组合对象和叶对象拥有相同的接口之外,对一组叶对象的操作也必须具有一致性。
  3. 双向映射关系:存在多对多情况时,建议使用中介者模式。
  4. 用职责链模式提高组合模式性能

模板方法模式

  • 定义:是一种只需使用继承就可以实现的非常简单的模式,由两部分结构组成,第一部分是抽象父类,第二部分是具体的实现子类。
var Beverage = function () {}

Beverage.prototype.boilWater = function () {
  console.log('把水煮沸')
}
Beverage.prototype.brew = function () {
  throw new Error('子类必须重写brew 方法')
}
Beverage.prototype.pourInCup = function () {
  throw new Error('子类必须重写pourInCup 方法')
}
Beverage.prototype.addCondiments = function () {
  throw new Error('子类必须重写addCondiments 方法')
}
Beverage.prototype.customerWantsCondiments = function () {
  return true // 默认需要调料
}
Beverage.prototype.init = function () {
  this.boilWater()
  this.brew()
  this.pourInCup()
  if (this.customerWantsCondiments()) { // 如果挂钩返回true,则需要调料
    this.addCondiments()
  }
}
var CoffeeWithHook = function () {}
CoffeeWithHook.prototype = new Beverage()
CoffeeWithHook.prototype.brew = function () {
  console.log('用沸水冲泡咖啡')
}
CoffeeWithHook.prototype.pourInCup = function () {
  console.log('把咖啡倒进杯子')
}
CoffeeWithHook.prototype.addCondiments = function () {
  console.log('加糖和牛奶')
}

CoffeeWithHook.prototype.customerWantsCondiments = function () {
  return window.confirm('请问需要调料吗?')
}
var coffeeWithHook = new CoffeeWithHook()
coffeeWithHook.init()

享元模式

  • 核心:是运用共享技术来有效支持大量细粒度的对象。

享元模式最关键的就是划分内部和外部状态,划分的标准如下:

  • 内部状态存储于对象内部。
  • 内部状态可以被一些对象共享。
  • 内部状态独立于具体的场景,通常不会改变。
  • 外部状态取决于具体的场景,并根据场景而变化,外部状态不能被共享。

享元模式例子:

var Upload = function (uploadType) {
  this.uploadType = uploadType
}

var UploadFactory = (function () {
  var flyObjs = {}

  return {
    create: function (uploadType) {
      if (flyObjs[uploadType]) {
        return flyObjs[uploadType]
      }
      return flyObjs[uploadType] = new Upload(uploadType)
    }
  }
})()

var UploadManage = function () {
  this.databaseObjs = {}

  return {
    add: function (id, uploadType, filename, filesize) {
      var flyobj = UploadFactory.add(uploadType)
      databaseObjs[id] = {
        filename: filename,
        filesize: filesize
      }
      return flyobj
    }
  }
}

什么时候使用享元模式:

  • 一个程序中使用了大量的相似对象。
  • 由于使用了大量对象,造成很大的内存开销。
  • 对象的大多数状态都可以变为外部状态。
  • 剥离出对象的外部状态之后,可以用相对较少的共享对象取代大量对象。

再看一个应用:

通用对象池:

var objPoolFactory = function (createFn) {
  var objectPool = []

  return {
    create: function () {
      var obj = objectPool.length > 0 ? objectPool.shift() : createFn.apply(this, arguments)
      return obj
    },
    recover: function (obj) {
      objectPool.push(obj)
    }
  }
}

var iframeFactory = objectPoolFactory(function () {
  var iframe = document.createElement('iframe')
  document.body.appendChild(iframe)
  iframe.onload = function () {
    iframe.onload = null // 防止iframe 重复加载的bug
    iframeFactory.recover(iframe) // iframe 加载完成之后回收节点
  }
  return iframe
})

职责链模式

  • 定义:使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系,将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它为止。
var order500 = function (orderType, pay, stock) {
  if (orderType === 1 && pay === true) {
    console.log('500 元定金预购,得到100 优惠券')
  } else {
    return 'nextSuccessor' // 我不知道下一个节点是谁,反正把请求往后面传递
  }
}

var order200 = function(orderType, pay, stock) {
  if (orderType === 2 && pay === true) {
    console.log('200 元定金预购,得到50 优惠券') 
  }else{
    return 'nextSuccessor' // 我不知道下一个节点是谁,反正把请求往后面传递
  }
}

var orderNormal = function(orderType, pay, stock) {
  if (stock > 0) {
    console.log('普通购买,无优惠券') 
  }else{
    console.log('手机库存不足') 
  }
}

var Chain = function (fn) {
  this.fn = fn
  this.successor = null
}

Chain.prototype.setNextSuccessor = function (successor) {
  return this.successor = successor
}

Chain.prototype.passRequest = function () {
  var ret = this.fn.apply(this, arguments)
  if (ret === 'nextSuccessor') {
    return this.successor && this.successor.passRequest.apply(this.successor, arguments)
  }
  return ret
}

var chainOrder500 = new Chain(order500)
var chainOrder200 = new Chain(order200)
var chainOrderNormal = new Chain(orderNormal)

chainOrder500.setNextSuccessor(chainOrder200)
chainOrder200.setNextSuccessor(chainOrderNormal)

职责链模式优点:

  • 链中的节点对象可以灵活地拆分重组
  • 可以手动指定起始节点

中介者模式

目的:

  • 增加一个中介者对象后,所有的相关对象都通过中介者对象来通信,而不是互相引用,所以当一个对象发生改变时,只需要通知中介者对象即可。
  • 中介者使各对象之间耦合松散,而且可以独立地改变它们之间的交互。
var goods = { // 手机库存
  "red|32G": 3,
  "red|16G": 0,
  "blue|32G": 1,
  "blue|16G": 6
}
var mediator = (function () {
  var colorSelect = document.getElementById('colorSelect'),
  memorySelect = document.getElementById('memorySelect'),
  numberInput = document.getElementById('numberInput'),
  colorInfo = document.getElementById('colorInfo'),
  memoryInfo = document.getElementById('memoryInfo'),
  numberInfo = document.getElementById('numberInfo'),
  nextBtn = document.getElementById('nextBtn')
  return {
    changed: function (obj) {
      var color = colorSelect.value, // 颜色
      memory = memorySelect.value,// 内存
      number = numberInput.value, // 数量
      stock = goods[ color + '|' + memory ] // 颜色和内存对应的手机库存数量
      if (obj === colorSelect) { // 如果改变的是选择颜色下拉框
        colorInfo.innerHTML = color
      }else if (obj === memorySelect) {
        memoryInfo.innerHTML = memory
      }else if (obj === numberInput) {
        numberInfo.innerHTML = number
      }
      if (!color) {
        nextBtn.disabled = true
        nextBtn.innerHTML = '请选择手机颜色'
        return
      }
      if (!memory) {
        nextBtn.disabled = true
        nextBtn.innerHTML = '请选择内存大小'
        return
      }
      if (((number - 0)  | 0)  !== number - 0) { // 输入购买数量是否为正整数
        nextBtn.disabled = true
        nextBtn.innerHTML = '请输入正确的购买数量'
        return
      }
      nextBtn.disabled = false
      nextBtn.innerHTML = '放入购物车'
    }
  }
})()

colorSelect.onchange = function () {
  mediator.changed(this)
}
memorySelect.onchange = function () {
  mediator.changed(this)
}
numberInput.oninput = function () {
  mediator.changed(this)
}

装饰者模式

  • 解释:装饰者模式将一个对象嵌入另一个对象之中,实际上相当于这个对象被另一个对象包装起来,形成一条包装链。请求随着这条链依次传递到所有的对象,每个对象都有处理这条请求的机会。

简单装饰者实例:

var Plane = function () {}

Plane.prototype.fire = function () {
  console.log('plane fire')
}

var MissileDecorator = function (plane) {
  this.plane = plane
}

MissileDecorator.prototype.fire = function () {
  this.plane.fire()
  console.log('MissileDecorator fire')
}

var AtomDecorator = function (plane) {
  this.plane = plane
}

AtomDecorator.prototype.fire = function () {
  this.plane.fire()
  console.log('AtomDecorator fire')
}

使用AOP实现装饰者:

Function.prototype.before = function (beforefn) {
  var _self = this
  return function () {
    beforefn.apply(this, arguments)
    return _self.apply(this, arguments)
  }
}

Function.prototype.after = function (afterfn) {
  var _self = this
  return function () {
    var ret = _self.apply(this, arguments)
    afterfn.apply(this, arguments)
    return ret
  }
}

状态模式

  • 定义:允许一个对象在其内部状态改变时改变它的行为,对象看起来似乎修改了它的类。

用状态模式写一个电灯程序:

var OffLightState = function (light) {
  this.light = light
}

OffLightState.prototype.buttonPress = function () {
  console.log('weak light')
  this.light.setState(this.light.WeakLightState)
}

var WeakLightState = function (light) {
  this.light = light
}

WeakLightState.prototype.buttonPress = function () {
  console.log('strong light')
  this.light.setState(this.light.StrongLightState)
}

var StrongLightState = function (light) {
  this.light = light
}

StrongLightState.prototype.buttonPress = function () {
  console.log('off light')
  this.light.setState(this.light.OffLightState)
}

var Light = function () {
  this.offLightState = new OffLightState(this)
  this.WeakLightState = new WeakLightState(this)
  this.StrongLightState = new StrongLightState(this)
  this.button = null
}

Light.prototype.init = function () {
  var button = document.createElement('button')
  var self = this

  this.button = document.body.appendChild(button)
  this.button.innerHTML = 'Switch'
  this.curState = this.offLightState // 设置初始状态
  this.button.onclick = function () {
    self.curState.buttonPress()
  }
}

Light.prototype.setState = function (state) {
  this.curState = state
}

var light = new Light()
light.init()

JavaScript里的状态模式:

这里也可以将状态抽成一个对象字面量使用:

var Light = function () {
  this.curState = FSM.off
  this.button = null
}

Light.prototype.init = function () {
  var button = document.createElement('button')
  var self = this

  this.button = document.body.appendChild(button)
  this.button.innerHTML = 'Switch'
  this.button.onclick = function () {
    self.curState.buttonPress.call(self)
  }
}

var FSM = {
  off: {
    buttonPress: function () {
      console.log('light on')
      this.curState = FSM.on
    }
  },
  on: {
    buttonPress: function () {
      console.log('light off')
      this.curState = FSM.off
    }
  }
}

var light = new Light()
light.init()

优点:

  • 状态模式定义了状态与行为之间的关系,并将它们封装在一个类里。通过增加新的状态类,很容易增加新的状态和转换。
  • 避免Context无限膨胀,状态切换的逻辑被分布在状态类中,也去掉了Context中原本过多的条件分支。
  • 用对象代替字符串来记录当前状态,使得状态的切换更加一目了然。
  • Context中的请求动作和状态类中封装的行为可以非常容易地独立变化而互不影响。

适配器模式

  • 目的:解决两个软件实体间的接口不兼容的问题。

原来的程序:

var googleMap = {
  show: function () {
    console.log('开始渲染谷歌地图')
  }
}
var baiduMap = {
  show: function () {
    console.log('开始渲染百度地图')
  }
}
var renderMap = function(map) {
  if (map.show instanceof Function) {
    map.show()
  }
}
renderMap(googleMap)
renderMap(baiduMap)

当baiduMap的api发生了变化,在不该动原有程序的基础上,我们可以使用适配器模式这样修改:

var googleMap = {
  show: function () {
    console.log('开始渲染谷歌地图')
  }
}
var baiduMap = {
  display: function () {
    console.log('开始渲染百度地图')
  }
}
var baiduAdapterMap = {
  show: function () {
    return baiduMap.display()
  }
}
var renderMap = function(map) {
  if (map.show instanceof Function) {
    map.show()
  }
}
renderMap(googleMap)
renderMap(baiduAdapterMap)

设计原则

单一职责原则:

  • 定义:就一个类而言,应该仅有一个引起它变化的原因。
  • 实践:代理模式、迭代器模式、单例模式和装饰者模式。
  • 优点:降低了单个类或者对象的复杂度,按照职责把对象分解成更小的粒度,这有助于代码的复用,也有利于进行单元测试
  • 缺点:会增加编写代码的复杂度。

最少知识原则:

  • 定义:是一个软件实体应当尽可能少地与其他实体发生相互作用。
  • 实践:中介者模式、外观模式。

开放-封闭原则:

  • 定义:软件实体(类、模块、函数)等应该是可以扩展的,但是不可修改。
  • 实践:发布——订阅模式、模板方法模式、策略模式、代理模式、职责链模式、装饰者模式、适配器模式。
  • 实现原则方法:1.放置钩子 2.使用回调函数

接口和面向接口编程:

  • 定义:以抽象类和实现类设计对象。
  • 作用:1.通过向上转型来隐藏对象的真正类型,以表现对象的多态性。 2.约定类与类之间的一些契约行为。

参考资料:

7分钟理解JS的节流、防抖及使用场景