设计模式结构型之装饰器模式
装饰器模式(Decorator Pattern),它不仅允许向一个现有的对象添加新的功能,同时又不会改变其结构。
要谈装饰器“模式”,不如我们直接来讲讲什么是装饰器,在你了解装饰器之后,就能明白所谓装饰器模式实际上就是应用装饰器的一种模式,或者说一种思路。
1.什么是装饰器?
首先假设这样一个场景,有一个函数 calculate(x) ,它可以传入一个参数 x,并且只要 x 相同的话返回结果就是一样的。
但是这个函数有一个缺点,就是每一次执行这个函数会耗费大量的时间,如果我们需要经常调用该函数,那么就会希望能把结果缓存下来以避免重复的计算。
那么我们该如何实现这个功能,让我们既可以实现数据的缓存调用,又可以不用改动 calculate 函数本身呢?
装饰器可以帮助我们。
首先,我们来定义 calculate 函数:
function calculate (x) {
// ... 执行代码
console.log("十分耗费性能!!!");
return x;
}
那么我们该如何具体的实现这样一个装饰器呢,像这样:
// 计算缓存 - 装饰器
function cachingDecorator (fn) {
// 我们用 map 结构来保存数据
const cache = new Map();
// 返回一个函数
return function(x) {
// 从缓存中读取
if (cache.has(x)) {
// 为了大家能方便的看到效果增加一条输出语句
console.log(`从缓存中读取:${cache.get(x)}`);
return cache.get(x);
}
// 执行
const result = fn.call(this, x);
// 为了大家能方便的看到效果增加一条输出语句
console.log(`新值:${result}`);
// 把新的值存入缓存
cache.set(x, result);
return result;
}
}
好了,最后再来测试一下
excute(1);
excute(1);
excute(1);
excute(2);
excute(2);
执行结果:
简单来分析一下吧:
1、当我们第一次执行 excute(1) 时,由于此时并没有缓存对应地键值对,那么会执行 calculate 这个函数,输出 => 十分耗费性能!!!。随后输出 => 新值:1。
2、当我们第二次,第三次执行 excute(1) 时,此时缓存中已经已经存放过该值,因此代码输出 => 从缓存中读取:1。
3、当我们想入新传入一个参数 2 时,因为没有缓存,则逻辑与第一点相同,此时输出 => 新值:2。
至此,对于装饰器的一个简单的例子就介绍完了。可以看到,我们并没有改变 calculate 这个函数,同时也实现了对计算结果的缓存。
回到文章一开始就给出的关于装饰器的定义:装饰器不仅允许向一个现有的对象添加新的功能,同时又不会改变其结构。相信此时大家应该能对装饰器的定义有一个更深的理解了。
2.React 高级组件(HOC)
高阶组件是参数为组件,返回值为新组件的函数。 – 引自 react 官网 HOC 部分
const EnhancedComponent = higherOrderComponent(WrappedComponent);
高阶组件的使用和装饰器的使用思路基本一致,这是因为 React 高阶组件本质就是装饰器。
关于 React 的高阶组件具体内容我们不在这里做扩展,我们的初始目的是为了介绍装饰器。
3.使用装饰器的注意点
在这里我们借用上面已经实现的计算缓存的装饰器:
function calculate (x) {
return x;
}
function cachingDecorator (fn) {
const cache = new Map();
return function(x) {
if (cache.has(x)) {
return cache.get(x);
}
const result = fn.call(this, x);
cache.set(x, result);
return result;
}
}
1、使用装饰器的情况下,需要注意原函数是否存在函数属性。
我们可以来试验一下,首先为 calculate 函数添加函数属性 description 用于对函数作用进行描述,就像这样:
function calculate (x) {
return x;
}
calculate.description = "这是一个十分耗费性能的计算函数!"
console.log("calculate.description:", calculate.description);
打印出来实际上能获取到 calculate.description 的值。
但是我们通过装饰器进行装饰之后,还可以拿到 description 吗?来看一下:
const excute = cachingDecorator(calculate);
console.log("excute.description:", excute.description); // undefined
聪明的你一定能发现,这里的 excute 已经是一个新函数,那么原函数的函数属性是没有的。
因此,如果你使用装饰器,那么需要注意原函数是否存在属性。不过这不是无法解决的,存在一种创建装饰器的方法,该装饰器可保留对函数属性的访问权限,但这需要使用特殊的 Proxy 对象来包装函数。
2、在 cachingDecorator 这个装饰器中,执行 fn 时我们使用了 fn.call() 的方式,这是为什么呢?
我们先来看看不使用这种方式,而是直接执行 fn 会不会有问题。
function cachingDecorator (fn) {
const cache = new Map();
return function(x) {
if (cache.has(x)) {
return cache.get(x);
}
const result = fn(x);
cache.set(x, result);
return result;
}
}
大家可以自行用我们上面的例子试一试,结果是一模一样的,这样子看上去好像没有什么问题呀!
但是大家试试这样定义 calculate 函数:
const cal = {
calName: "计算",
calculate(x) {
console.log("十分耗费性能!!!", this.calName);
return x;
}
}
// ...
cal.calculate = cachingDecorator(cal.calculate);
cal.calculate(1);
cal.calculate(1);
cal.calculate(1);
cal.calculate(2);
cal.calculate(2);
大家可以自行跑一下代码,你会发现,为什么这里的 this.calName 会是 undefined 呢?
我们再吧 fn 执行的方法改回 fn.call(this, x) ,再来看一下结果吧:
可以看到这里的 this.calName 成功获取到值。
这里简单的解释一下,有一个特殊的内建函数方法 func.call(context, …args),它允许调用一个显式设置 this 的函数。通过 call 的调用才能保证 this 不会丢失。
类似于 call 的内建函数还有 apply 和 bind,感兴趣的话大家可以自行在网上查一查相关文章。
最后,也许你已经发现,装饰器模式就是针对装饰器的应用,并且通过前面几节的学习,大家应该可以感觉到,所谓设计模式,好像就是一套成熟的程序设计和实现的思路。
如果你对设计模式仍然没有感觉,没关系,先继续往下看吧。