面向切面編程(AOP)是 Aspect Oriented Programming 的縮寫(xiě),脫胎于函數(shù)式編程,是一種無(wú)侵入式的編程風(fēng)格。
無(wú)侵入式編程
所謂無(wú)侵入式編程,就是在不修改原有代碼的基礎(chǔ)上,對(duì)原始代碼進(jìn)行一些功能拓展。??梢詰?yīng)用于諸如代碼統(tǒng)計(jì)、日志打印、原始函數(shù)功能補(bǔ)充等場(chǎng)景中。
下面通過(guò)一些示例來(lái)展示這種編程風(fēng)格在 JavaScript 中的應(yīng)用。
舉個(gè)吃飯的例子
這里舉一個(gè)吃飯的例子,你正在做一個(gè)關(guān)于“吃”的項(xiàng)目,項(xiàng)目中你定義了一個(gè) eat 函數(shù):
function eat(){
console.log("我正在吃糖醋排骨,好香好香")
}
隨后隨著需求升級(jí),PM 要求你完善整個(gè)吃飯的流程,一個(gè)比較完整的吃飯流程應(yīng)該是:做飯、吃飯、刷碗。此處你只定義了“吃”的方法,新需求還要求你實(shí)現(xiàn)“做飯”和“刷碗”兩項(xiàng)功能。
你可以直接對(duì) eat 函數(shù)進(jìn)行修改,在函數(shù)開(kāi)頭加入“做飯”的功能,在函數(shù)結(jié)尾加入“刷碗”的功能,但你知道這樣是不好的,因?yàn)檫@種方式侵入了原始的 eat 函數(shù),降低了代碼的可維護(hù)性,也增加了出錯(cuò)的可能性。
不讓修改原始的 eat 函數(shù)怎么辦?此時(shí)就需要一種無(wú)侵入式的編程范式,也就是本文要介紹的 AOP 編程。
JavaScript 中 AOP 的實(shí)現(xiàn)
在 JavaScript 中實(shí)現(xiàn) AOP 編程,主要是通過(guò) Function.prototype 完成。此例中,我們可以在 Function.prorotype 上掛載一個(gè) before 方法和 after 方法,分別在 eat 函數(shù)執(zhí)行前后調(diào)用。
Function.prototype.before = function(fn){
fn && fn();
this()
}
Function.prototype.after = function(fn){
this()
fn && fn();
}
此時(shí)如果我們調(diào)用 before 函數(shù):
eat.before(()=>{
console.log("我先去做飯")
});
輸出結(jié)果為:
我先去做飯
我正在吃糖醋排骨,好香好香
同理,如果我們調(diào)用 after 函數(shù):
eat.after(()=>{
console.log("我得去刷碗了")
});
輸出結(jié)果為:
我正在吃糖醋排骨,好香好香
我得去刷碗了
現(xiàn)在,我們已經(jīng)初步實(shí)現(xiàn)了一個(gè) AOP 的編程模型。
實(shí)現(xiàn)鏈?zhǔn)秸{(diào)用
上面的代碼有沒(méi)有什么問(wèn)題呢?在我看來(lái),至少有兩個(gè)問(wèn)題:
- 同時(shí)調(diào)用
before函數(shù)和after函數(shù),會(huì)導(dǎo)致eat函數(shù)被調(diào)用兩次 - 無(wú)法實(shí)現(xiàn)鏈?zhǔn)秸{(diào)用
上面的代碼,如果只是單獨(dú)調(diào)用 before 或 after 函數(shù),沒(méi)有什么問(wèn)題。但新流程是需要我們同時(shí)調(diào)用 before 和 after 方法的:
eat.before(()=>{
console.log("我先去做飯")
});
eat.after(()=>{
console.log("我得去刷碗了")
});
下面是運(yùn)行結(jié)果:
我先去做飯
我正在吃糖醋排骨,好香好香
我正在吃糖醋排骨,好香好香
我得去刷碗了
可見(jiàn),eat 函數(shù)被調(diào)用了兩次,如果我們想在“做飯”之前在做點(diǎn)操作,比如“買(mǎi)菜”,會(huì)導(dǎo)致 eat 函數(shù)多次的調(diào)用。究其原因,是因?yàn)槲覀冋{(diào)用 before 或 after 函數(shù)時(shí),會(huì)立即調(diào)用 eat 函數(shù),我們可以對(duì) before 函數(shù)和 after 函數(shù)做一些修改,讓它們返回一個(gè)函數(shù):
Function.prototype.before = function(fn){
return () => {
fn && fn();
this()
}
}
Function.prototype.after = function(fn){
return () => {
this()
fn && fn();
}
}
通過(guò)返回一個(gè)函數(shù),實(shí)現(xiàn)了 eat 的延遲調(diào)用,同時(shí)由于返回值是一個(gè)函數(shù),我們還可以對(duì)該返回值應(yīng)用 before 和 after 函數(shù),并實(shí)現(xiàn)鏈?zhǔn)秸{(diào)用,這得益于 JavaScript 的靈活性。
修改代碼后,我們進(jìn)行以下調(diào)用:
eat.before(()=>{
console.log("我先去做飯")
}).after(()=>{
console.log("我得去刷碗了")
})();
你可以把 before 和 after 作為一個(gè)包裝器,類(lèi)似于 bind 方法,都是綁定在 Function .prototype 上的方法,對(duì)調(diào)用這些函數(shù)的函數(shù)(this)進(jìn)行一些包裝,返回一個(gè)新的函數(shù)數(shù),而原始的函數(shù)不受影響。
現(xiàn)在我們實(shí)現(xiàn)了一個(gè)具有一定擴(kuò)展性的 AOP 編程模型,此例中,如果你想在“做飯”之前加上“買(mǎi)菜”功能,在“刷碗”后加上“看電視”功能,以及其他任意的功能,都可以很方便的添加:
eat.before(()=>{
console.log("我先去做飯")
}).after(()=>{
console.log("我得去刷碗了")
}).before(()=>{
console.log("我先去買(mǎi)菜")
}).after(()=>{
console.log("我去休息一下")
})();
運(yùn)行結(jié)果:
我先去買(mǎi)菜
我先去做飯
我正在吃糖醋排骨,好香好香
我得去刷碗了
我去休息一下
函數(shù)返回值
經(jīng)過(guò)一連串的鏈?zhǔn)秸{(diào)用后,怎么獲取原始函數(shù)的返回值呢?如果我們的 eat 函數(shù)有個(gè)返回值:
function eat(){
console.log("我正在吃糖醋排骨,好香好香");
return {
"dish":"糖醋排骨"
}
}
要想獲取原始函數(shù)的返回值,我們?cè)賹?duì) before 和 after 做一些修改:
Function.prototype.before = function(fn){
return () => {
fn && fn();
const result = this();
return result;
}
}
Function.prototype.after = function(fn){
return () => {
const result = this();
fn && fn();
return result;
}
}
這里對(duì) before 和 after 函數(shù)返回的函數(shù)增加了返回值,返回值就是調(diào)用該函數(shù)的函數(shù)(這里是 eat)的返回值,這樣一來(lái),eat 函數(shù)的返回值就可以層層進(jìn)行傳遞下去了:
const retVal = eat.before(()=>{
console.log("我先去做飯")
}).after(()=>{
console.log("我得去刷碗了")
}).before(()=>{
console.log("我先去買(mǎi)菜")
}).after(()=>{
console.log("我去休息一下")
})();
console.log(retVal)
調(diào)用結(jié)果如下:
我先去買(mǎi)菜
我先去做飯
我正在吃糖醋排骨,好香好香
我得去刷碗了
我去休息一下
{ dish: '糖醋排骨' }
實(shí)例:代碼統(tǒng)計(jì)
通過(guò)前面的介紹,我們實(shí)現(xiàn)了一個(gè) AOP 的編程模型,盡管這個(gè)模型并不是盡善盡美的,但我們已經(jīng)可以用來(lái)做一些事情了。我們來(lái)做一個(gè)實(shí)現(xiàn)代碼統(tǒng)計(jì)的例子,通過(guò)代碼統(tǒng)計(jì),可以找出一些運(yùn)行時(shí)間較長(zhǎng)的函數(shù),然后對(duì)其進(jìn)行優(yōu)化。這里我們準(zhǔn)備對(duì) test 函數(shù)進(jìn)行代碼統(tǒng)計(jì):
function test(){
for(let i = 0; i < 100000000;i++){}
}
我們可以實(shí)現(xiàn)一個(gè) getRunTime 方法,該方法用來(lái)進(jìn)行代碼統(tǒng)計(jì):
function getRunTime(fn){
// 定義開(kāi)始時(shí)間和結(jié)束時(shí)間
let start;
let end;
// 使用獲取代碼運(yùn)行時(shí)間
fn.before(()=>{
start = +new Date();
}).after(()=>{
end = +new Date();
})();
return end - start;
}
然后只需調(diào)用 getRunTime 方法,就可以實(shí)現(xiàn)代碼統(tǒng)計(jì)了:
const runTime = getRunTime(test)
console.log(runTime)
運(yùn)行結(jié)果:
66
getRunTime 的內(nèi)部實(shí)現(xiàn)是依賴(lài)于 AOP 的,通過(guò) AOP 實(shí)現(xiàn)了無(wú)侵入式的編程,我們?cè)跊](méi)有對(duì) test 內(nèi)部代碼進(jìn)行任何修改的情況下,實(shí)現(xiàn)了對(duì)其的運(yùn)行時(shí)長(zhǎng)進(jìn)行統(tǒng)計(jì),這種編程范式的優(yōu)點(diǎn),大家可以細(xì)細(xì)體會(huì)。
總結(jié)
本文對(duì)面向切面編程(AOP)的概念進(jìn)行了一些介紹,并給出了 JavaScript 的實(shí)現(xiàn)方式,最后給出了一個(gè)例子。
在 JavaScript 中實(shí)現(xiàn) AOP 的原理很簡(jiǎn)單,就是通過(guò)對(duì) Function.prototype 進(jìn)行擴(kuò)展,這種方式也應(yīng)用于諸多內(nèi)置函數(shù)中,如常用的 bind 函數(shù),我們應(yīng)該主要學(xué)習(xí)這種思想。
在 Python 語(yǔ)言中,裝飾器也是一種面向切面編程的范式,我在這篇文章中也進(jìn)行了一些介紹,大家可以對(duì)比學(xué)習(xí)。
完。