《面試題系列》淺談裝飾器
一個(gè)朋友一面遇到的面試題,還挺有意思,簡單分享下。由一個(gè)簡單的場(chǎng)景,把一些基礎(chǔ)知識(shí)點(diǎn)串了下。
實(shí)現(xiàn)函數(shù)緩存
假設(shè)有函數(shù) calculate,對(duì)于相同的入?yún)ⅲ偸悄芊祷叵嗤慕Y(jié)果。但每次計(jì)算很耗性能,希望簡單設(shè)計(jì)一個(gè)緩存的方案。
function?calculate?(a)?{
??const?b?=?耗性能的計(jì)算(a)
??return?b;
}
第一種最直觀的實(shí)現(xiàn)
緩存,顧名思義,將結(jié)果存起來,再次調(diào)用,如果命中緩存,直接從緩存取,否則返回結(jié)果,并緩存起來。
let?cache?=?new?Map();
function?calculate?(a)?{
??const?b?=?耗性能的計(jì)算(a);
??if?(cache.has[a])?return?cache.get(a);
??cache.set(a,?b);
??return?b;
}
但是這樣對(duì)原方法有侵入性,假如現(xiàn)在有很多方法都需要加緩存呢?
第二種裝飾器的實(shí)現(xiàn)
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;
??}
}
其實(shí)代碼跟第一種幾乎一樣,只是換了種思路,但是復(fù)用性更強(qiáng)了。假如 calculate 函數(shù)的入?yún)⑹菍?duì)象,并且要考慮緩存回收的問題,那么換成 WeakMap 就比較合適。
第三種解決 this 丟失的實(shí)現(xiàn)
第二種會(huì)有一個(gè)問題,假如 calculate 函數(shù)是這樣的。
const?case?=?{
??easy()?{
????return?200;
??},
??
??calculate(a)?{
????const?b?=?耗性能的計(jì)算(a);
????return?this.easy()?+?b;
??}
}
//?假如直接使用第二種的實(shí)現(xiàn):
case.calculate?=?decorator(case.calculate);
//?此時(shí)調(diào)用會(huì)報(bào)錯(cuò)
//?嚴(yán)格模式下:?Uncaught?TypeError:?Cannot?read?properties?of?undefined?(reading?'easy')
//?非嚴(yán)格模式下:Uncaught?TypeError:?this.easy?is?not?a?function
case.calculate(0);
//?既然是?this?丟失了,那么我們改寫一下第二種的實(shí)現(xiàn)
function?decorator(targetFn)?{
??let?cache?=?new?Map();
??
??return?function?(a)?{
????if?(cache.has(a))?return?cache.get(a);
????
????//?const?b?=?targetFn(a);
????//?上面注釋的換成下面這行,指定?this?調(diào)用就好了
????const?b?=?targetFn.call(this,?a);
????
????cache.set(a,?b);
????
????return?b;
??}
}
這一種已經(jīng)挺完整了,像 lodash 中的 debounced 方法,就是裝飾器這種結(jié)構(gòu)。
第四種解決屬性丟失的實(shí)現(xiàn)
還有一個(gè)問題,沒解決。假如被裝飾函數(shù)有自己的一些屬性,那么經(jīng)過 decorator 包裝之后,這些屬性會(huì)丟失。
function?calculate?()?{}
calculate.tag?=?'water';
const?calculate?=?decorator(calculate);
//?calculate.tag?為?undefined
我們可以改造 decorator 函數(shù),利用 Proxy 來實(shí)現(xiàn)。
//?假設(shè)?calculate?是這個(gè)樣子
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
裝飾器模板
經(jīng)過上面一系列的流程,我們可以簡單總結(jié)出一個(gè)裝飾器模板。
function?decoratorTemplate?(targetFn)?{
??//?輔助變量(閉包特性)
??
??//?Proxy?作代理攔截
??return?new?Proxy(targetFn,?{
????apply(target,?thisArg,?args)?{
??????//?apply?對(duì)函數(shù)進(jìn)行包裝
??????target.apply(thisArg,?args);
????}
??});
}
比如,我們想實(shí)現(xiàn)函數(shù)執(zhí)行完成之后,打印一條 log。
function?logCompleted?()?{};
function?decoratorTemplate?(targetFn)?{
??//?輔助變量(閉包特性)
??
??//?Proxy?作代理攔截
??return?new?Proxy(targetFn,?{
????apply(target,?thisArg,?args)?{
??????//?apply?對(duì)函數(shù)進(jìn)行包裝
??????target.apply(thisArg,?args);
??????console.log('logs:?completed');
????}
??});
}
logCompleted?=?decoratorTemplate(logCompleted);
logCompleted();?//?logs:?completed
再比如,我們想統(tǒng)計(jì)函數(shù)執(zhí)行的次數(shù)。
function?logCompleted?()?{};
function?decoratorTemplate?(targetFn)?{
??//?輔助變量(閉包特性)
??let?count?=?0;
??
??//?Proxy?作代理攔截
??return?new?Proxy(targetFn,?{
????apply(target,?thisArg,?args)?{
??????//?apply?對(duì)函數(shù)進(jìn)行包裝
??????target.apply(thisArg,?args);
??????console.log(`logs:?${count}`);
??????count?+=?1;
????}
??});
}
logCompleted?=?decoratorTemplate(logCompleted);
logCompleted();?//?logs:?0
logCompleted();?//?logs:?1
小結(jié)
近期有不少朋友在面試,盡量把一些有意思的題目,跟大家分享下。
像這種由場(chǎng)景展開的面試題個(gè)人覺得比較好,一上來就直接問概念的那種,不知道小伙伴遇到過沒?留言區(qū)歡迎交流
