Js中的装饰者模式
定义
装饰者模式也称为包装器模式,在不改变原有对象的基础上为其动态的添加上新的功能。
详细描述
在我们平时开发项目时想要扩展一个对象或者函数时(函数也是对象),可以直接修改原来的对象使其拥有新的功能,可是这样是不符合开放封闭原则的,而且随着业务越来越多目标对象会变的非常的庞大和复杂,变得难以维护。装饰者模式就是来解决这种问题,他会将新添加的功能定义为新的装饰类,然后使用装饰类来装饰(包装)一下原有对象,使原有对象可以轻易的拥有装饰类中的功能,并且自身不会改变。
举个现实中的例子,我们自己本身是一个对象,当天气冷的时候我会想穿上毛衣,下雨的时候在穿上雨衣,当天晴了以后在把他们脱掉。这里的毛衣和雨衣都可以看成是一个个装饰类,他们把我们本身的对象包装了一下使我们不怕冷并且还拥有防雨的功能,可是他们不是我们对象自身自带的,所以当我们需要和不需要的时候才可以更容易的自由使用和移除。如下图:
应用场景:
- 当对象的功能需要动态的添加,又需要动态的撤销时。
- 当需要对一系列功能进行排列组合时。
- 当需要在现有功能前/后增加新的功能时。
代码实例
装饰对象
下面我们就来模拟实现一个穿衣服的实例,先不使用装饰者模式如下:
// 对象本身
let self = {
wear() {
console.log('自身啥也没穿-光腚');
},
};
// 穿上毛衣
self = {
wear() {
console.log('自身啥也没穿-光腚');
console.log('太冷了,赶紧穿上毛衣');
},
};
// 穿上雨衣
self = {
wear() {
console.log('自身啥也没穿-光腚');
console.log('太冷了,赶紧穿上毛衣');
console.log('妈呀还下雨了,在穿上雨衣');
},
};
self.wear();
// 打印
// 自身啥也没穿-光腚
// 太冷了,赶紧穿上毛衣
// 妈呀还下雨了,在穿上雨衣
首先我们定义了一个自身对象-身上啥也没穿,此时太冷了要穿上毛衣就需要深入对象内部去修改来添加毛衣,下雨了想穿雨衣也需要去对象内部方法修改来增加穿上雨衣功能。也就是说每次功能的增加和减少都需要深入对象内部的方法去修改,只要深入对象内部修改就需要更多的考虑新的修改是否会对原来的代码有哪些影响。这样是不符合开放-封闭原则的。
下面我们看下装饰者模式的实现:
// 对象本身
const self = {
wear() {
console.log('自身啥也没穿-光腚');
},
};
// 穿上毛衣
const sweater = () => {
console.log('太冷了,赶紧穿上毛衣');
};
// 穿上雨衣
const raincoat = () => {
console.log('妈呀还下雨了,在穿上雨衣');
};
const wear1 = self.wear;
self.wear = function () {
wear1();
sweater();
};
const wear2 = self.wear;
self.wear = function () {
wear2();
raincoat();
};
self.wear();
// 打印
// 自身啥也没穿-光腚
// 太冷了,赶紧穿上毛衣
// 妈呀还下雨了,在穿上雨衣
上述代码根据js语言自身的特点,通过保存原引用的方式实现了功能的包装,但是自身对象原有的wear方法内部没有做任何改变,就可以拥有了穿上毛衣和雨衣的功能。
装饰函数
在js中函数也是对象,我们日常开发中充满了各种方法的提取封装修改等等,有的时候我们想在不改动函数本身的同时增加一些额外的功能。如我们想监听一下onload事件,并在事件触发时执行自己的逻辑,但是此时又不确定别人有没有已经绑定过onload事件,自己会不会把他们的覆盖,此时可以使用装饰者模式实现,如:
window.onload = function () {
console.log('我是不知道谁添加的功能');
};
// 通过引用地址保存原函数
let _onload = window.onload || function () {};
window.onload = function () {
_onload(); // this指向
console.log('我在onload事件中添加了自己的功能--hhhh');
};
上面通过保存引用地址的方法实现装饰onload函数的功能,但是在执行的时候会有一些隐藏问题,比如_onload调用的时候其实函数的this指向会改变,这里只是因为刚好都指向window所以没有出问题,其他函数就需要手动的去绑定this指向比较费事。下面我们使用AOP切面函数来实现函数装饰,并且很好的解决这个问题。
AOP装饰函数
我们来使用AOP对函数进行装饰,使原函数执行之前/后添加新的功能,并且不改变原函数自身代码。
我们在来看下上面绑定onload事件的实现:
// 定义AOP装饰函数
Function.prototype.before = function (beforeFn) {
const _self = this; // 保存原函数引用
// 负责函数执行顺序
return function (...params) {
beforeFn.apply(this, params); // 插入之前函数执行
_self.apply(this, params); // 执行原函数
};
};
window.onload = function () {
console.log('我是不知道谁添加的功能');
};
// 使用装饰函数装饰
window.onload = (window.onload || function () {})
.before(function () {
console.log('我在onload事件中添加了自己的功能--hhhh');
})
.before(function () {
console.log('我在添加一个功能2222');
});
// 打印
// 我在添加一个功能2222
// 我在onload事件中添加了自己的功能--hhhh
// 我是不知道谁添加的功能
上述代码首先定义一个切片装饰函数,然后直接将需要添加的函数功能传入进去即可,而且还支持链式添加,更加的简洁方便。
虽然上面的方法已经很方便了但是还有个问题是我们直接在Function的原型上添加的方法,这样的话很可能会对原型中的属性造成污染,所以下面我们在实现一个例子并且去解决这个问题。比如我们实现一个小时吃饭的例子,妈妈警告我们吃饭之前必须洗手,想要出去玩必须先吃完饭。代码如下:
const before = Symbol('before');
const after = Symbol('before');
// 定义AOP装饰函数
Function.prototype[before] = function (beforeFn) {
const _self = this; // 保存原函数引用
// 负责函数执行顺序
return function (...params) {
beforeFn.apply(this, params); // 插入之前函数执行
_self.apply(this, params); // 执行原函数
};
};
// 定义AOP装饰函数
Function.prototype[after] = function (afterFn) {
const _self = this; // 保存原函数引用
// 负责函数执行顺序
return function (...params) {
_self.apply(this, params); // 执行原函数
afterFn.apply(this, params); // 插入之后函数执行
};
};
let eat = function () {
console.log('好好吃饭长高高');
};
const wash = function () {
console.log('必须先洗手,不然不给吃饭');
};
const play = function () {
console.log('终于吃完了,我要去玩玩玩玩');
};
eat = eat[before](wash)[after](play);
eat();
// 打印
// 必须先洗手,不然不给吃饭
// 好好吃饭长高高
// 终于吃完了,我要去玩玩玩玩
上面我们就实现了一个在吃饭行为之前通过包装拥有洗手行为,吃饭后去玩耍的例子,并且使用Symbol 语法解决了上述可能会污染函数原型的问题。
总结
优点
- 可以动态的给原对象添加功能,非常灵活。
- 添加新功能的同时不会修改原对象,符合开闭原则。
- 装饰对象与原对象松耦合,易于维护。
缺点
- 定义过多的装饰类,会增加系统的复杂性。
装饰者模式在我们日常开发中非常的有用,尤其是我们在接手一个新项目时,遇到一大堆代码时非常有用。