《面试题系列》浅谈装饰器
接水怪
共 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
小结
近期有不少朋友在面试,尽量把一些有意思的题目,跟大家分享下。
像这种由场景展开的面试题个人觉得比较好,一上来就直接问概念的那种,不知道小伙伴遇到过没?留言区欢迎交流
评论