4、设计模式结构型之装饰器模式

设计模式结构型之装饰器模式

 
装饰器模式(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
 
简单来分析一下吧:
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 呢?
2
 
我们再吧 fn 执行的方法改回 fn.call(this, x) ,再来看一下结果吧:
3

 

可以看到这里的 this.calName 成功获取到值。

这里简单的解释一下,有一个特殊的内建函数方法 func.call(context, …args),它允许调用一个显式设置 this 的函数。通过 call 的调用才能保证 this 不会丢失。

类似于 call 的内建函数还有 apply 和 bind,感兴趣的话大家可以自行在网上查一查相关文章。

 

最后,也许你已经发现,装饰器模式就是针对装饰器的应用,并且通过前面几节的学习,大家应该可以感觉到,所谓设计模式,好像就是一套成熟的程序设计和实现的思路。

如果你对设计模式仍然没有感觉,没关系,先继续往下看吧。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值