《面试题系列》浅谈装饰器

接水怪

共 3225字,需浏览 7分钟

 ·

2022-02-27 22:28

一个朋友一面遇到的面试题,还挺有意思,简单分享下。由一个简单的场景,把一些基础知识点串了下。

实现函数缓存

假设有函数 calculate,对于相同的入参,总是能返回相同的结果。但每次计算很耗性能,希望简单设计一个缓存的方案。

function calculate (a) {
  const b = 耗性能的计算(a)
  return b;
}

第一种最直观的实现

缓存,顾名思义,将结果存起来,再次调用,如果命中缓存,直接从缓存取,否则返回结果,并缓存起来。

let cache = new Map();
function calculate (a) {
  const b = 耗性能的计算(a);
  if (cache.has[a]) return cache.get(a);
  cache.set(a, b);
  return b;
}

但是这样对原方法有侵入性,假如现在有很多方法都需要加缓存呢?

第二种装饰器的实现

function decorator(targetFn) {
  let cache = new Map();
  
  return function (a) {
    if (cache.has(a)) return cache.get(a);
    
    const b = targetFn(a);
    cache.set(a, b);
    
    return b;
  }
}

其实代码跟第一种几乎一样,只是换了种思路,但是复用性更强了。假如 calculate 函数的入参是对象,并且要考虑缓存回收的问题,那么换成 WeakMap 就比较合适。

第三种解决 this 丢失的实现

第二种会有一个问题,假如 calculate 函数是这样的。

const case = {
  easy() {
    return 200;
  },
  
  calculate(a) {
    const b = 耗性能的计算(a);
    return this.easy() + b;
  }
}

// 假如直接使用第二种的实现:
case.calculate = decorator(case.calculate);

// 此时调用会报错
// 严格模式下: Uncaught TypeError: Cannot read properties of undefined (reading 'easy')
// 非严格模式下:Uncaught TypeError: this.easy is not a function
case.calculate(0);

// 既然是 this 丢失了,那么我们改写一下第二种的实现
function decorator(targetFn) {
  let cache = new Map();
  
  return function (a) {
    if (cache.has(a)) return cache.get(a);
    
    // const b = targetFn(a);
    // 上面注释的换成下面这行,指定 this 调用就好了
    const b = targetFn.call(this, a);
    
    cache.set(a, b);
    
    return b;
  }
}

这一种已经挺完整了,像 lodash 中的 debounced 方法,就是装饰器这种结构。

第四种解决属性丢失的实现

还有一个问题,没解决。假如被装饰函数有自己的一些属性,那么经过 decorator 包装之后,这些属性会丢失。

function calculate () {}
calculate.tag = 'water';

const calculate = decorator(calculate);
// calculate.tag 为 undefined

我们可以改造 decorator 函数,利用 Proxy 来实现。

// 假设 calculate 是这个样子
function calculate(x) {
  return x + 1;
}

calculate.tags = 'water';

function decorator(targetFn) {
  let cache = new Map();

  // 装饰器部分改用 Proxy 来做代理拦截
  return new Proxy(targetFn, {
    apply(target, thisArgs, args) {
      if (cache.has(args[0])) return cache.get(args[0]);

      const b = target.apply(thisArgs, args);
      cache.set(args[0], b);

      return b;
    }
  })
}

calculate = decorator(calculate);
console.log(calculate.tags); // water

装饰器模板

经过上面一系列的流程,我们可以简单总结出一个装饰器模板。

function decoratorTemplate (targetFn) {
  // 辅助变量(闭包特性)
  
  // Proxy 作代理拦截
  return new Proxy(targetFn, {
    apply(target, thisArg, args) {
      // apply 对函数进行包装
      target.apply(thisArg, args);
    }
  });
}

比如,我们想实现函数执行完成之后,打印一条 log。

function logCompleted () {};

function decoratorTemplate (targetFn) {
  // 辅助变量(闭包特性)
  
  // Proxy 作代理拦截
  return new Proxy(targetFn, {
    apply(target, thisArg, args) {
      // apply 对函数进行包装
      target.apply(thisArg, args);
      console.log('logs: completed');
    }
  });
}

logCompleted = decoratorTemplate(logCompleted);
logCompleted(); // logs: completed

再比如,我们想统计函数执行的次数。

function logCompleted () {};

function decoratorTemplate (targetFn) {
  // 辅助变量(闭包特性)
  let count = 0;
  
  // Proxy 作代理拦截
  return new Proxy(targetFn, {
    apply(target, thisArg, args) {
      // apply 对函数进行包装
      target.apply(thisArg, args);
      console.log(`logs: ${count}`);
      count += 1;
    }
  });
}

logCompleted = decoratorTemplate(logCompleted);
logCompleted(); // logs: 0
logCompleted(); // logs: 1

小结

近期有不少朋友在面试,尽量把一些有意思的题目,跟大家分享下。

像这种由场景展开的面试题个人觉得比较好,一上来就直接问概念的那种,不知道小伙伴遇到过没?留言区欢迎交流64d4090c9a1700906582f65c03682f9b.webp


浏览 14
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报