装饰者模式能够动态地给某个对象添加一些额外的职责,而不会影响这个类中派生的其余对象。装饰模式可以在不改变对象自身的基础上,在程序运行期间给对象动态地添加职责,跟继承相比,装饰者更加轻便灵活。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实现上面例子的代码以下: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)
}
复制代码
这样的代码是符合开放-封闭原则的,咱们增长新的功能的时候,没有修改原来的代码。但这种方式有一些缺点:异步
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')
复制代码
首先定义两个函数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的经典应用之一。在项目的开发结尾的时候,咱们通常须要加一些统计数据的代码,这些过程可能让咱们被迫改动已经封装好的函数。好比,页面中有登陆按钮,点击登陆按钮,弹出登陆弹窗的同时还要上报数据,来统计有多少用户点击了登陆按钮。下面简单的代码实现:
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
复制代码
观察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中独特的装饰者模式,这种模式在实际开发中很是有用。