原文链接:http://www.jianshu.com/p/20381c6b9251
面向切面编程(AOP)是 Aspect Oriented Programming 的缩写,脱胎于函数式编程,是一种无侵入式的编程风格。
无侵入式编程
所谓无侵入式编程,就是在不修改原有代码的基础上,对原始代码进行一些功能拓展。常可以应用于诸如代码统计、日志打印、原始函数功能补充等场景中。
下面通过一些示例来展示这种编程风格在 JavaScript 中的应用。
举个吃饭的例子
这里举一个吃饭的例子,你正在做一个关于“吃”的项目,项目中你定义了一个eat函数:
function eat(){
console.log("我正在吃糖醋排骨,好香好香")
}
随后随着需求升级,PM 要求你完善整个吃饭的流程,一个比较完整的吃饭流程应该是:做饭、吃饭、刷碗。此处你只定义了“吃”的方法,新需求还要求你实现“做饭”和“刷碗”两项功能。
你可以直接对eat函数进行修改,在函数开头加入“做饭”的功能,在函数结尾加入“刷碗”的功能,但你知道这样是不好的,因为这种方式侵入了原始的eat函数,降低了代码的可维护性,也增加了出错的可能性。
不让修改原始的eat函数怎么办?此时就需要一种无侵入式的编程范式,也就是本文要介绍的 AOP 编程。
JavaScript 中 AOP 的实现
在 JavaScript 中实现 AOP 编程,主要是通过Function.prototype完成。此例中,我们可以在Function.prorotype上挂载一个before方法和after方法,分别在eat函数执行前后调用。
Function.prototype.before = function(fn){
fn && fn();
this()
}
Function.prototype.after = function(fn){
this()
fn && fn();
}
此时如果我们调用before函数:
eat.before(()=>{
console.log("我先去做饭")
});
输出结果为:
我先去做饭
我正在吃糖醋排骨,好香好香
同理,如果我们调用after函数:
eat.after(()=>{
console.log("我得去刷碗了")
});
输出结果为:
我正在吃糖醋排骨,好香好香
我得去刷碗了
现在,我们已经初步实现了一个 AOP 的编程模型。
实现链式调用
上面的代码有没有什么问题呢?在我看来,至少有两个问题:
同时调用before函数和after函数,会导致eat函数被调用两次
无法实现链式调用
上面的代码,如果只是单独调用before或after函数,没有什么问题。但新流程是需要我们同时调用before和after方法的:
eat.before(()=>{
console.log("我先去做饭")
});
eat.after(()=>{
console.log("我得去刷碗了")
});
下面是运行结果:
我先去做饭
我正在吃糖醋排骨,好香好香
我正在吃糖醋排骨,好香好香
我得去刷碗了
可见,eat函数被调用了两次,如果我们想在“做饭”之前在做点操作,比如“买菜”,会导致eat函数多次的调用。究其原因,是因为我们调用before或after函数时,会立即调用eat函数,我们可以对before函数和after函数做一些修改,让它们返回一个函数:
Function.prototype.before = function(fn){
return () => {
fn && fn();
this()
}
}
Function.prototype.after = function(fn){
return () => {
this()
fn && fn();
}
}
通过返回一个函数,实现了eat的延迟调用,同时由于返回值是一个函数,我们还可以对该返回值应用before和after函数,并实现链式调用,这得益于 JavaScript 的灵活性。
修改代码后,我们进行以下调用:
eat.before(()=>{
console.log("我先去做饭")
}).after(()=>{
console.log("我得去刷碗了")
})();
你可以把before和after作为一个包装器,类似于bind方法,都是绑定在Function .prototype上的方法,对调用这些函数的函数(this)进行一些包装,返回一个新的函数数,而原始的函数不受影响。
现在我们实现了一个具有一定扩展性的 AOP 编程模型,此例中,如果你想在“做饭”之前加上“买菜”功能,在“刷碗”后加上“看电视”功能,以及其他任意的功能,都可以很方便的添加:
eat.before(()=>{
console.log("我先去做饭")
}).after(()=>{
console.log("我得去刷碗了")
}).before(()=>{
console.log("我先去买菜")
}).after(()=>{
console.log("我去休息一下")
})();
运行结果:
我先去买菜
我先去做饭
我正在吃糖醋排骨,好香好香
我得去刷碗了
我去休息一下
函数返回值
经过一连串的链式调用后,怎么获取原始函数的返回值呢?如果我们的eat函数有个返回值:
function eat(){
console.log("我正在吃糖醋排骨,好香好香");
return {
"dish":"糖醋排骨"
}
}
要想获取原始函数的返回值,我们再对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;
}
}
这里对before和after函数返回的函数增加了返回值,返回值就是调用该函数的函数(这里是eat)的返回值,这样一来,eat函数的返回值就可以层层进行传递下去了:
const retVal = eat.before(()=>{
console.log("我先去做饭")
}).after(()=>{
console.log("我得去刷碗了")
}).before(()=>{
console.log("我先去买菜")
}).after(()=>{
console.log("我去休息一下")
})();
console.log(retVal)
调用结果如下:
我先去买菜
我先去做饭
我正在吃糖醋排骨,好香好香
我得去刷碗了
我去休息一下
{ dish: '糖醋排骨' }
实例:代码统计
通过前面的介绍,我们实现了一个 AOP 的编程模型,尽管这个模型并不是尽善尽美的,但我们已经可以用来做一些事情了。我们来做一个实现代码统计的例子,通过代码统计,可以找出一些运行时间较长的函数,然后对其进行优化。这里我们准备对test函数进行代码统计:
function test(){
for(let i = 0; i < 100000000;i++){}
}
我们可以实现一个getRunTime方法,该方法用来进行代码统计:
function getRunTime(fn){
// 定义开始时间和结束时间
let start;
let end;
// 使用获取代码运行时间
fn.before(()=>{
start = +new Date();
}).after(()=>{
end = +new Date();
})();
return end - start;
}
然后只需调用getRunTime方法,就可以实现代码统计了:
const runTime = getRunTime(test)
console.log(runTime)
运行结果:
66
getRunTime的内部实现是依赖于 AOP 的,通过 AOP 实现了无侵入式的编程,我们在没有对test内部代码进行任何修改的情况下,实现了对其的运行时长进行统计,这种编程范式的优点,大家可以细细体会。
总结
本文对面向切面编程(AOP)的概念进行了一些介绍,并给出了 JavaScript 的实现方式,最后给出了一个例子。
在 JavaScript 中实现 AOP 的原理很简单,就是通过对Function.prototype进行扩展,这种方式也应用于诸多内置函数中,如常用的bind函数,我们应该主要学习这种思想。
在 Python 语言中,装饰器也是一种面向切面编程的范式,我在这篇文章中也进行了一些介绍,大家可以对比学习。
作者:黑黢黢
链接:http://www.jianshu.com/p/20381c6b9251
來源:
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。