Skip to content

18-装饰器模式、转发、call和apply

JavaScript 在处理函数时提供了非凡的灵活性。它们可以被传递,用作对象,现在我们将看到如何在它们之间 转发(forward) 调用并 装饰(decorate) 它们。

装饰模式是一种结构型设计模式, 允许通过将对象放入包含行为的特殊封装对象中来为原对象绑定新的行为

装饰器 - 透明缓存

举一个装饰器模式的例子:假设我们有一个 CPU 重负载的函数 slow(x),但它的结果是稳定的。换句话说,对于相同的 x,它总是返回相同的结果。

js
function slow(x) {
  // 这里可能会有重负载的 CPU 密集型工作
  alert(`Called with ${x}`);
  return x;
}

如果经常调用该函数,我们可能希望将结果缓存(记住)下来,以避免在重新计算上花费额外的时间。

但是我们不是将这个功能添加到 slow() 中,而是创建一个包装器(wrapper)函数,该函数增加了缓存功能。正如我们将要看到的,这样做有很多好处。

js
function cachingDecorator(func) {
  let cache = new Map();

  return function(x) {
    if(cache.has(x)) {
      return cache.get(x);
    } 
    // 否则就调用 func
    let res = func(x);

    cache.set(x, res);
    return res;
  }
}

slow = cachingDecorator(slow); // 用装饰器对原函数进行处理并返回新的函数

在上面的代码中,cachingDecorator 是一个装饰器(decorator)

装饰器(decorator)

一个特殊的函数,它接受另一个函数并改变它的行为。

其中,返回的函数是一个包装器(wrapper)

从外部代码来看,包装的 slow 函数执行的仍然是与之前相同的操作。它只是在其行为上添加了缓存功能,通过这种方式添加额外的功能的好处在于:

  • 装饰器是可重用的,可以应用于另一个需要该功能的函数。
  • 缓存逻辑是独立的,没有增加原函数的复杂性。
  • 多个装饰器可以组合使用。

func.call

上面👆提到的装饰器方法适用于普通函数,不适用于对象的方法。

例如,在下面的代码中:

js
// 我们将对 worker.slow 的结果进行缓存
let worker = {
  someMethod() {
    return 1;
  },

  slow(x) {
    // 可怕的 CPU 过载任务
    console.log('Called with ' + x);
    return x * this.someMethod(); // (*)
  },
};

// 和之前例子中的代码相同
function cachingDecorator(func) {
  let cache = new Map();
  return function (x) {
    if (cache.has(x)) {
      return cache.get(x);
    }
    let result = func(x); // (**)
    cache.set(x, result);
    return result;
  };
}

console.log(worker.slow(1)); // 原始方法有效

worker.slow = cachingDecorator(worker.slow); // 现在对其进行缓存

console.log(worker.slow(2));

代码的输出为:

可以看出在对 worker.slow 进行装饰后会报错,这是由于是包装器将原始函数调用为 (**) 行中的 func(x)。并且,当这样调用时,函数将得到 this = window,也就是执行 (*) 时没有正确的上下文 this ,因此发生了错误。

为了解决这个问题,JavaScript 提供了一个内建函数方法 func.call(context, ...args),语法为:

js
func.call(context, arg1, arg2, ...)

它运行 func,提供的第一个参数作为运行的上下文 this,后面的作为参数。

func.call 的作用

将上下文传递给函数,让函数在指定的上下文中执行。

js
function cachingDecorator(func) {
  let cache = new Map();
  return function (x) {
    if (cache.has(x)) {
      return cache.get(x);
    }
    let result = func.call(this, x); // (**)
    cache.set(x, result);
    return result;
  };
}

worker.slow = cachingDecorator(worker.slow); // 对其进行缓存

console.log(worker.slow(2));
console.log(worker.slow(2)); // 正确使用缓存

输出为:

这样子就一切都正常工作了,我们再深入看看 this 是如何被传递的:

    1. 在经过装饰之后,worker.slow 现在是包装器 function (x) { ... }
  1. 因此,当 worker.slow(2) 执行时,包装器将 2 作为参数,并且 this=worker(它是点符号 . 之前的对象)。
  2. 在包装器内部,假设结果尚未缓存,func.call(this, x) 将当前的 this=worker)和当前的参数(=2)传递给原始方法。

func.call 需要一个参数一个参数传递,func.apply 可以把多个参数合并为一个参数数组。

总结

装饰器 是一个围绕改变函数行为的包装器。主要工作仍由该函数来完成。

装饰器可以被看作是可以添加到函数的 “features” 或 “aspects”。我们可以添加一个或添加多个。而这一切都无需更改其代码!

为了实现 cachingDecorator,我们研究了以下方法:

参考