装饰者模式(Decorator)
装饰者模式可以动态给某个对象添加一些额外的职责,而不会影响从这个类中派生的其他对象。
还有一种添加功能的方式是继承。继承这种功能复用方式通常被称为”白箱复用“,”白箱“是相对可见性而言的,在继承方式中,超类的内部细节是对子类可见的,继承常常被认为破坏了封装性。
装饰者模式能够在不改变对象自身的基础上,在程序运行期间给对象动态地添加职责。
设计原则:多用组合,少用继承。
优点:
- 动态为对象添加行为
缺点:
- 往往会形成一条长长的装饰链,会对性能上有一定的影响。
JS模拟传统面向对象语言中的装饰者模式
例:一辆战斗机一开始是发送普通子弹,随着升级开始发送原子弹和导弹。
const Plane = function() {}
Plane.prototype.fire = function() {
console.log('发射普通子弹');
}
const MissileDecorator = function(plane) {
this.plane = plane
}
MissileDecorator.prototype.fire = function() {
this.plane.fire()
console.log('发射导弹');
}
const AtomDecorator = function(plane) {
this.plane = plane
}
AtomDecorator.prototype.fire = function() {
this.plane.fire()
console.log('发射原子弹');
}
let plane = new Plane()
plane = new MissileDecorator(plane)
plane = new AtomDecorator(plane)
plane.fire()
这种给对象动态增加职责的方式,并没有真正地改动对象自身,而是将对象放入另一个对象之中,这些对象以一条链的方式进行引用,形成一个聚合对象。
装饰者模式将一个对象嵌入另一个对象之中,实际上相当于这个对象被另一个对象包装起来,形成一条包装链。请求随着这条链依次传递到所有的对象,每个对象都有处理这条请求的机会。
在JS中的实现
比如在 window
绑定 onload
事件,但又不确定这个时间是不是已经被其他人绑定过,为了避免覆盖掉之前的 onload
函数中的行为,所以事先会保存原来的 onload
函数。
window.onload = function() {
alert(1)
}
const _onload = window.onload || function() {} // 保存原先 onload 函数
window.onload = function() {
_onload()
alert(2)
}
存在两个问题:
- 必须维护中间变量(如:
_onload
) - 有时候会存在
this
指向问题。
用AOP装饰函数解决
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 ret = _self.apply(this, arguments) // 并返回原函数的结果
afterfn.apply(this, arguments) // 执行新函数
return ret
}
}
// -------------分割线---------------
// 有一些人不喜欢这种污染原型的方法,也可以改成这样:
const before = function (fn, beforefn) {
return function () {
beforefn.apply(this, arguments) // 执行新函数
return fn.apply(this, arguments) // 并返回原函数的结果
}
}
// 使用的时候 a = before(a, function () {})
使用:
<button id="btn">按钮</button>
<script>
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 ret = _self.apply(this, arguments) // 并返回原函数的结果
afterfn.apply(this, arguments) // 执行新函数
return ret
}
}
document.getElementById = document.getElementById.before(function () {
alert(1)
})
const btn = document.getElementById('btn');
</script>
<!-- 打开浏览器后,会弹出警告框,因为在getElementById这个方法添加了弹出的功能 -->
应用实例
数据上报
比如要统计登录按钮被点击了多少次,按一般的思路,我们会在按钮的点击事件中调用数据上报的方法,如:
document.getElementById('btn').addEventListener('click', function () {
// TODO 打开登录弹窗
// TODO 数据上报
})
毫无疑问,这么实现是没有问题的,但两个层面的的功能被耦合在一个函数里,既要打开弹窗,又要上报。
我们可以使用 AOP 分离:
Function.prototype.after = function (afterfn) {
const _self = this
return function () {
const ret = _self.apply(this, arguments)
afterfn.apply(this, arguments)
return ret
}
}
let handleLogin = () => {
// TODO 打开登陆弹窗
}
const log = () => {
// TODO 数据上报
}
handleLogin = handleLogin.after(log)
document.getElementById('btn').addEventListener('click', handleLogin)
完成~😊
插件式表单验证
Function.prototype.before = function (beforefn) {
const _self = this
return function () {
if (beforefn.apply(this, arguments) === false) {
// 当beforefn 返回 false 的情况直接return,不执行后续原函数
return
}
return _self.apply(this, arguments)
}
}
const validata = function () {
if (username.value === '') {
alert('用户名不能为空')
return false
}
if (password.value === '') {
alert('密码不能为空')
return false
}
}
const formSubmit = function () {
// TODO fetch login
}
formSubmit = formSubmit.before(validata)
btn.onclick = function () {
formSubmit()
}
注意:函数通过
Function.prototype.before
或者Function.prototype.after
被装饰之后,返回的实际上是一个新的函数,如果原函数上保存了一些属性,那么这些属性会丢失。
装饰者模式和代理模式的区别
这两种模式都描述了为对象提供一定程度上的间接引用,它们的实现部分都保留了对另外一个对象的引用,并且向那个对象发送请求。
区别:
- 代理模式目的是,当直接访问本体不方便或者不符合需要时,为这个本体提供一个替代者。本体定义了关键功能,而代理提供或拒绝对它的访问,或者在访问本体之前做一些额外的事情。
- 装饰者模式的作用就是为对象动态加入行为。
代理模式通常只是一层代理-本体的引用,而装饰者模式经常会形成一条长长的装饰链。