JavaScript设计模式之装饰者模式

定义

装饰者模式能够动态地给某个对象添加一些额外的职责,而不会影响这个类中派生的其余对象。装饰模式可以在不改变对象自身的基础上,在程序运行期间给对象动态地添加职责,跟继承相比,装饰者更加轻便灵活。javascript

使用面向对象实现装饰者模式

假设咱们编写一个飞机大战的游戏,飞机会根据经验值的增长升级子弹的类型,一开始飞机只能发射普通子弹,升到二级能够发射导弹,升到三级能够发射原子弹。用代码实现以下:java

var Plane = function () {}

Plane.prototype.fire = function () {
  console.log('发射子弹')
}

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

MissleDecorator.prototype.fire = function () {
  this.plane.fire()
  console.log('发射导弹')
}

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

AtomDecorator.prototype.fire = function () {
  this.plane.fire()
  console.log('发射原子弹')
}

// 应用
let plane = new Plane()
plane = new MissleDecorator(plane)
plane = new AtomDecorator(plane)
plane.fire() // 发送普通子弹、发送导弹、发送原子弹
复制代码

导弹和原子弹装饰类的构造函数都接受plane对象,而且保存这个参数,在它们的fire方法中,除了自身的操做,还要调用plane对象的fire方法。这种方式没有改变plane对象的自身,而是将对象传递给另外一个对象,这些对象以一条链的方式进行引用,造成聚合对象。
能够看到装饰者对象和它所装饰的对象拥有一致的接口,因此它们对使用该对象的客户来讲是透明的,被装饰对象也不须要知道它曾经被装饰过,这种透明性使得咱们能够嵌套任意多个装饰对象。web

JavaScript中的装饰者

JavaScript语言的动态性使得改变对象很容易,咱们能够直接改写对象或者某个对象的方法,并不须要用“类”来装饰,使用JavaScript实现上面例子的代码以下:ajax

const plane = {
  fire () {
    console.log('发射子弹')
  }
}

const missleDecorator = function () {
  console.log('发射导弹')
}

const atomDecorator = function () {
  console.log('发射原子弹')
}

const copyFire1 = plane.fire
plane.fire = function () {
  copyFire1()
  missleDecorator()
}

const copyFir2 = plane.fire
plane.fire = function () {
  copyFire2()
  atomDecorator()
}

plane.fire() // 发送普通子弹、发送导弹、发送原子弹
复制代码

装饰函数

在JavaScript中,几乎一切都是对象,函数又被称为一等对象。在JavaScript中能够很方便地修改对象的属性和方法,因此要为函数添加功能,最简单粗暴的方式是直接改写函数,可是这违反开放-封闭原则。好比下面的例子:算法

var a = function () {
  console.log(1)
}
// 改为
var a = function () {
  console.log(1)
  console.log(2)
}
复制代码

可是若是某个函数很复杂,并且以前可能也不是你维护,随便修改极可能产生难以预料的Bug,因而咱们从装饰者模式中找到了一种答案,保存原函数的引用,而后添加新的功能:服务器

var a = function () {
  console.log(1)
}

var _a = a

a = function () {
  _a()
  console.log(2)
}
复制代码

在实际开发中,这也是一种常见的作法。好比咱们想给window绑定onload事件,可是不肯定这个事件是否是被其余人绑定过,因而为了以前的函数不被覆盖,有以下代码:app

window.onload = function () {
  console.log(1)
}

var _onload = window.onload || function () {}
window.onload = function () {
  _onload()
  console.log(2)
}
复制代码

这样的代码是符合开放-封闭原则的,咱们增长新的功能的时候,没有修改原来的代码。但这种方式有一些缺点:异步

  • 必须维护_onload这个中间变量,若是函数的装饰链较长,或者装饰的函数变多,这些中间变量的数量也会愈来愈多。
  • 还会有this被劫持的问题,在上面的例子中没有问题,由于调用普通函数_onload,this也指向window,如今把window.onload改为document.getElementById,代码以下:
var _getElementById = document.getElementById

document.getElementById = function (id) {
  console.log(1)
  return _getElementById(id)
}

var button = document.getElementById('button')
复制代码

执行这段代码,控制台在打印1后,抛出以下异常:函数

// Uncaught TypeError: Illegal invocation
复制代码

异常的缘由就是此时_getElementById是一个全局函数,调用全局函数时,this指向window的,而document.getElementById内部this预期的指向是document。因此咱们须要改进代码:post

var _getElementById = document.getElementById

document.getElementById = function (id) {
  console.log(1)
  return _getElementById.call(document, id)
}

var button = document.getElementById('button')
复制代码

使用AOP装饰函数

首先定义两个函数Function.prototype.before和Function.prototype.after:

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 () {
    const ret = self.apply(this, arguments)
    afterFn.apply(this, arguments)

    return ret
  }
}
复制代码

Function.prototype.before接受一个函数做为参数,这个函数即为要添加的装饰函数,它里面有须要添加的新功能的代码。
接着把当前的this保存起来,这个this指向原函数,返回一个代理函数。这个代理函数的做用是把请求分别转发给新添加的函数和原函数,而且保证它们的执行顺序,让新添加的函数在原函数以前执行,也叫前置装饰,这样就实现了动态装饰的效果。
由于咱们在函数中保存了this,经过apply函数绑定正确的this,保证函数在被装饰以后,this不会被劫持。因而前面的例子,咱们能够这样写:

document.getElementById = document.getElementById.before(function () {
   console.log(1)
 })

 console.log(document.getElementById('button'))
复制代码

AOP应用

数据统计上报

分离业务代码和数据统计代码,不管在什么语言中,都是AOP的经典应用之一。在项目的开发结尾的时候,咱们通常须要加一些统计数据的代码,这些过程可能让咱们被迫改动已经封装好的函数。好比,页面中有登陆按钮,点击登陆按钮,弹出登陆弹窗的同时还要上报数据,来统计有多少用户点击了登陆按钮。下面简单的代码实现:

function showLoginModal () {
  console.log('打开登陆弹窗')
  log('传入一些按钮信息')
}

function log (info) {
  console.log('上报用户信息和按钮信息到服务器')
}

document.getElementById('loginBtn').onclick = showLoginModal
复制代码

能够看到,在showLogin函数里,既要负责打开弹窗的功能,又要负责数据上报,这两个不一样层面的代码耦合在一块儿,咱们可使用AOP进行优化:

var showLoginModal = function () {
  console.log('打开登陆弹窗')
  log('传入一些按钮信息')
}

function log (info) {
  console.log('上报用户信息和按钮信息到服务器')
}

showLoginModal = showLoginModal.after(log)  // 打开弹窗以后上报数据

document.getElementById('loginBtn').onclick = showLoginModal
复制代码

使用AOP动态改变函数的参数

观察Function.prototype.before函数:

Function.prototype.before = function (beforeFn) {
  var self = this

  return function () {
    beforeFn.apply(this, arguments)

    return self.apply(this, arguments)
  }
}
复制代码

能够看到beforeFn函数和原函数共用参数arguments,因此咱们在beforeFn中修改参数后,原函数接收的参数也会发生变化。
如今有一个用于ajax请求的函数,它负责项目中全部的ajax异步请求:

var ajax = function (type, url, param) {
  console.dir(param)

  // 这里是发送请求的代码
}

ajax('get', 'http://xxx.com/userInfo', { name: 'uzi' })
复制代码

上面代码表示向服务端发起一个获取用户信息的请求,传递的参数是{ name: 'uzi' }。ajax函数在项目中一直工做良好,忽然有有一天,网站遭受了CSRF攻击,解决CSRF的一个办法就是在全部HTTP请求中带上一个token参数。因而咱们定义了一个生成token的函数:

var getToken = function () {
  return 'token'
}
复制代码

下面给全部请求加上token参数:

var ajax = function (type, url, param) {
  param = param || {}
  param.token = getToken()

  // 这里是发送请求的代码
}
复制代码

这样问题就解决了,可是ajax函却变得僵硬了,虽然每一个ajax请求都自动带上了token参数,在当前项目是没有什么问题。可是,若是未来要将这个ajax函数封装到公司的通用库里,那这个token参数可能就是多余的了,也许另外一个项目不须要token参数,或者生成token的算法不同,不管怎么样,都须要修改这个ajax函数。咱们用AOP来解决这个问题:

var ajax = function (type, url, param) {
  console.dir(param)

  // 这里是发送请求的代码
}

var getToken = function () {
  return 'token'
}

// 使用Function.prototype.before装饰ajax函数
ajax = ajax.before(function (type, url, param) {
  param.token = getToken()
})

ajax('get', 'http://xxx.com/userInfo', { name: 'uzi' })  // { name: 'uzi', token: 'token'}
复制代码

这样咱们就保证了ajax函数的干净,提升了ajax函数的复用性,而且也知足了添加token的需求。

插件式的表单验证

表单验证在web开发中是一个很常见的需求,好比在一个登陆页面,咱们在把用户的数据,好比用户名、密码等信息提交给服务器以前,就会常常须要作校验,假设咱们如今只须要校验字段是否为空,因而有以下代码:

var username = document.getElementById('username')
var password = document.getElementById('password')
var submitBtn = document.getElementById('submitBtn')

function submitHandler () {
  if (username.value === '') {
    return alert('用户名不能为空')
  }
  if (password.value === '') {
    return alert('密码不能为空')
  }
  ajax('post', 'http://xxx.com/login', { username: username.value, password: password.value })
}

submitBtn.onclick = submitHandler
复制代码

上面的submitHandler在此处承担了两个职责,除了ajax的请求以外,还要验证用户输入的合法性,这种函数首先一旦校验的字段不少,代码就会臃肿,并且函数职责也很混乱,没法复用。 下面使用AOP进行优化,首先分离校验相关的代码:

function validateField () {
   if (username.value === '') {
    alert('用户名不能为空')
    return false
  }
  if (password.value === '') {
    alert('密码不能为空')
    return false
  }
}

function submitHandler () {
  var params = { username: username.value, password: password.value }
  ajax('post', 'http://xxx.com/login', params)
}
复制代码

改写前面的Function.prototype.before:

Function.prototype.before = function (beforeFn) {
  var self = this

  return function () {
    const ret = beforeFn.apply(this, arguments)
    if (ret === false) {
      return
    }

    return self.apply(this, arguments)
  }
}
复制代码

再用validateField前置装饰submitHandler:

submitHandler = submitHandler.before(validateField)

submitBtn.onclick = submitHandler
复制代码

这样咱们就完美将校验的代码和提交ajax请求的代码彻底分离开来,它们再也不有耦合关系,这样咱们在项目中能够把一些校验函数封装起来,达到复用的目的。

装饰者模式和代理模式

装饰者模式和代理模式的结构看起来很像,这两种模式都描述了怎么样为对象提供必定程度上的间接引用,它们的实现部分保留了对另外一个对象的引用,而且客户是直接向那个对象发送请求。

代码模式和装饰者模式最重要的区别是在于它们的意图和设计目的。代理模式的目的是,当直接访问本体不方便时或者不合符需求时,为本体提供一个替代者。本体定义了核心的功能,而代理提供的做用一个是直接拒绝一些访问,另外一个就是在本体以前作一些额外的事情。而装饰者的做用是给对象动态添加行为,能够说代理模式强调一种本体和代替者的一种能够静态表达的关系,这种关系在一开始就基本被肯定了。而装饰者模式一开始并不能肯定全部的功能,在不一样的场景中,可能会根据须要添加不一样的装饰者,这些装饰者能够造成一条长长的装饰链。

总结

经过上面的三个应场景:数据上报、动态改变函数参数以及表单校验,咱们能够看到在JavaScript中,咱们了解了装饰函数,了解了AOP,他们就是JavaScript中独特的装饰者模式,这种模式在实际开发中很是有用。

相关文章
相关标签/搜索