装饰器模式(Decorator Pattern)允许向一个现有的对象 添加新的功能,同时又不改变其结构。这种类型的设计模式属于 结构型模式,它是作为现有的类的一个包装;
这种模式创建了一个装饰类,用来包装原有的类,并在保持类方法签名 完整性 的前提下,提供了 额外的功能。
装饰器模式的 重点在于不改变原有的 结构 和 功能,现在需要一个办法,在不改变函数源代码的情况下,能给 函数增加功能,这正符合开放封闭原则;
总结要点:
1 为对象 添加新功能;
2 不改变原有的 结构 和 功能。
生活中示例 : 手机壳: 不改变手机原本的功能,只是添加了一些新功能
添加了一个 设置红色边框的 新功能:
在实际开发中还有一种 很常见的做法 使用了装饰器模式:
比如我们想给 window 绑定 onload 事件,但是又不确定这个事件是不是已经被其他人绑定过,为了避免覆盖掉之前的 window.onload 函数中的行为,我们 一般都会先保存好原先的 window.onload,把它 放入新的 window.onload 里执行:
这种做法 非常常见, 需要好好掌握;
比如 在 Vue 中, 对于 7 种操作数组的方法,我们 对它进行了功能的扩充:arrayMethods ⾸先继承了 Array ,然后对数组中所有能改变数组自⾝的方法, 对这 7中方法 进行重写。
重写后的方法会先执⾏它们 本⾝原有的逻辑,并对能 增加 数组长度的 3 个方法 push、unshift、splice ⽅法做了判断,获取到 插如的值(也就是对新值进行响应式),然后把新添加的值变成⼀个响应式对象,并且再调用 ob.dep.notify() 手动触发依赖通知。
又比如 在一些使用的 第三方库 中, 常常会用到这种模式 (vconsole npm 库):
问题
像上面那样处理会带来以下几个问题
1. 需要维护 类似 _onload、_open、_send 这样的 中间变量,就目前来说 算是小事,但是如果函数的装饰链较长,或者需要装饰的函数变多,这些中间变量的数量也会越来越多;
2. this 的指向问题,在 window.onload 和 XMLHttpRequest 的例子中没有这个问题,因为它们执行的时候都是指向 window,但是在其他的例子中就会出现:
_getElementById 是一个全局函数,当调用一个全局函数时,this 是指向 window 的,而 document.getElementById 方法的内部实现需要使用 this 引用,this 在这个方法内预期是指向 document,而不是 window, 这是错误发生的原因;
所以我们需要重新指定 this 的 指向:
还有一种完美的方式给 函数动态增加功能 的方式:用 AOP (面向切面编程) 装饰函数
Function.prototype.before 接受一个函数当作参数,这个函数即为新添加的函数,它 装载了新添加的功能代码;
接着 把当前的 this 保存起来,这个 this 指向 原函数,然后返回一个“代理”函数,这个“代理”函数只是结构上像代理而已,并不承担代理的职责(比如控制对象的访问等)。它的工作是把 请求分别转发给 新添加的函数 和 原函数,且负责保证它们的 执行顺序,让新添加的函数在原函数之前执行(前置装饰),这样就实现了 动态装饰 的效果;
同样的 Function.prototype.after 就是 后置装饰,原理一样;
看个例子,重构上面的 _getElementById 代码:
通过 显示 的指定 this ( document.getElementById = XXX, this 为 document),来动态指定 函数内部的 this。
上面的 AOP 实现是在 Function.prototype 上添加 before 和 after 方法,但许多人不喜欢这种污染原型的方式,那么我们可以做一些变通,把原函数和新函数都作为参数传入 before 或者 after 方法:
用 AOP 动态改变函数的参数
beforefn 和原函数 __self 共用一组参数列表 arguments,当我们在 beforefn 的函数体内改变 arguments 的时候,原函数 __self 接收的参数列表自然也会变化;
一个实际的应用场景
发送 ajax 请求:
如果需要添加 token 或者其他的参数 上面这么做 暂时没有问题;
但是 每个请求都会发送 token, 并且如果将来把这个函数移植到其他项目上,或者把它放到一个开源库中供其他人使用,Token 参数都将是多余的;
使用 AOP 动态更改函数参数:
AOP 的应用实例
用 AOP 装饰函数的技巧 在实际开发中非常有用。不论是业务代码的编写,还是在框架层面,我们都可以把行为依照 职责分成粒度更细的函数,随后通过装饰把它们合并到一起,这有助于我们编写一个 松耦合 和 高复用性 的系统;
1. 数据统计上报系统
场景 :点击按钮, 打开浮层 并且 进行上报
一般做法
这种做法出现的问题就是 在 showLogin 函数里,既要负责打开登录浮层,又要负责数据上报,这是两个层面的功能,在此处却被 耦合 在一个函数里;
使用 AOP 分离 之后, 隔离了 showLogin 和 log 这两个方法:
2. 插件式表单校验
formSubmit 函数在此处承担了两个职责,除了提交 ajax 请求之外,还要验证用户输入的合法性。这种代码一来会造成函数臃肿,职责混乱,二来谈不上任何可复用性;
现在的代码已经有了一些改进,我们把校验的逻辑都放到了 validata 函数中,但 formSubmit 函数的内部还要计算 validata 函数的返回值,因为返回值的结果表明了是否通过校验;
最终的 这段代码,使 validata 和 formSubmit 完全分离开来。改写 Function.prototype.before,如果 beforefn 的执行结果返回 false,表示不再执行后面的原函数;
注意:
1 函数通过 Function.prototype.before 或者 Function.prototype.after 被装饰之后,返回的实际上是一个新的函数,如果在原函数上保存了一些属性,那么这些属性会丢失;
2 这种装饰方式也叠加了函数的作用域,如果装饰的链条过长,性能上也会受到一些影响;
个人感觉 : 如果是重写原型的话,还是可以是用 非 AOP 形式, 这种 在很多库中都是这么使用的; 如果有一些 业务相关, 系统设计之类的 ,可以使用 AOP 的这种形式;
ES7 装饰器
配置环境
npm install babel-plugin-transform-decorators-legacy --save-dev
配置 .babelrc 文件
装饰类
带参数的装饰器
示例 mixin
装饰方法
1. 属性的限制:
2. 添加新的功能
core-decorators 库
详见 :https://github.com/jayphelps/core-decorators
第三方开源 lib , 提供常用得装饰器;
设计原则验证
将 现有对象 和 装饰器 进行分离,两者独立存在;
符合开放封闭原则。
文中参考 和 摘抄
JavaScript 设计模式与开发实践;