本文摘于《JS设计模式与开发实践》
JS语言动态改变对象相当容易,我们可以直接改写对象或对象的某个方法,并不需要使用“类”来实现装饰者模式。假如我们需要编写一个飞机大战的游戏,随着经验值的增加,我们操作的飞机对象可以升级成更厉害的飞机,一开始飞机只能发射普通的子弹,升到二级时可以发射导弹,升到三级可以发射原子弹,代码如下:
const plane = {
fire: function() {
console.log('发射普通子弹');
}
}
const missleDecorator = () => {
console.log('发射导弹');
}
const atomDecorator = () => {
console.log('发射原子弹');
}
const fire1 = plane.fire
plane.fire = () => {
fire1()
missleDecorator()
}
const fire2 = plane.fire
plane.fire = () => {
fire2()
atomDecorator()
}
plane.fire()// 分别输出:发射普通子弹、发射导弹、发射原子弹
AOP装饰函数
在JS中,几乎一切都是对象,其中函数又被称为一等对象。在JS中,可以很方便地给对象增加属性和方法,但却很难在不改动某个函数源代码的情况下给该函数增加额外的功能。在代码的运行期间,我们很难切入某个函数的执行环境。
如果直接改写该函数从而添加功能,那么这是最差的办法,直接违反了开放-封闭原则。很多时候我们不想去碰原函数,因为这可能是同事甚至前同事写的。因此,我们使用AOP装饰函数来解决这个问题
首先,给出Function.prototype.before和Function.prototype.after方法:
Function.prototype.before = function(beforefn) {
const _self = this
return function() {
beforefn.apply(this,arguments)
return _self.apply(this,arguments)
}
}
Function.prototype.after = function(afterfn) {
const _self = this
return function() {
const res = _self.apply(this,arguments)
afterfn.apply(this,arguments)
return res
}
}
它们接受一个函数作为参数,这个函数就是新添加的函数,装载了新添加的功能代码。通过apply来传递正确的this,这样就实现了动态装饰的效果:
function test() {
console.log('do sth');
}
test = (test || function() {}).after(() => {
console.log('do sth after');
}).before(() => {
console.log('do sth before');});
test()
//do sth before
//do sth
//do sth after
值得一提的是,上面的AOP实现是在Function.prototype上添加方法,但许多人不喜欢这种污染原型的方式,那么我们可以做一下变通,把原函数和新函数都作为参数传入before或者after方法:
before = (fn,beforefn) => {
return function() {
beforefn.apply(this,arguments)
return fn.apply(this,arguments)
}
}
after = (fn,afterfn) => {
return function() {
const res = fn.apply(this,arguments)
afterfn.apply(this,arguments)
return res
}
}
AOP的应用实例
1. 数据统计上报
分离业务代码和数据统计代码无论在什么语言中都是AOP的经典应用之一。在项目开发的收尾阶段难免会加上很多统计数据的代码,这些过程可能使我们被迫改动封装好的函数,使用AOP来装饰已封装好的函数可以避免这一点
2. 动态改变函数的参数
从上面的before函数中可以看到,新函数beforefn和原函数fn都使用了参数列表arguments,当在beforefn函数体中修改arguments时,原函数接收的参数列表自然也会变化。比如:给Ajax请求加上token参数
3. 插件式的表单验证
使用AOP装饰函数可以分离校验输入和提交Ajax请求的代码,我们把校验输入的逻辑放到validate函数中,并约定当validate返回false时,表示校验未通过,代码如下:
Function.prototype.before = function(beforefn) {
const _self = this
return function() {
if(beforefn.apply(this,arguments) === false) return
return _self.apply(this,arguments)
}
}
const validate = () => {
if(username.value === '') {
alert('用户名不能为空')
return false
}
if(password.value === '') {
alert('密码不能为空')
return false
}
}
formSubmit = formSubmit.before(validate)
submitBtn.onClick = () => {
formSubmit()
}