你所不知道的JavaScript面向切面编程

JavaScript面向切面编程小实战

Posted by wang chong on March 3, 2019

Aspect Orientend Programming(AOP)

AOP(面向切面编程)是一个比较热门的话题,它的主要作用是把一些跟核心业务逻辑模块无关的功能抽离出来,这些跟业务逻辑无关的功能通常包括日志统计、安全控制、异常处理等。把这些功能抽离出来之后, 再通过“动态织入”的方式掺入业务逻辑模块中。

在前端中面向切面编程热度并不是很好,但是它对于我们做一些事情是非常有必要的。

面向切面编程值得关注的地方就是它可以对我们的代码做无侵入式的干扰,即:不对具体业务进行干扰,只对业务操作,不在业务里面进行埋点。也就是说在函数之前和之后进行操作,如果哪一天热插拔把这个函数抽掉,对原来的业务逻辑没有任何影响。

下面来实战一下面向切面编程。

由于在JavaScript中我们使用函数来作为代码块或者一个模块,因此对于AOP主要是针对函数,所以在函数原型链上编程能够更好的实现。

AOP实战

比如说有这么一个函数,我们需要统计这个函数的执行时间是多少?

function test(){
    console.log(2);
}

before

在原型链上编程,我们可以定义函数执行之前的方法。

Function.prototype.before = function(fn,...argsSelf){
    fn();                   //先执行fn
    this.apply(this,argsSelf);  //再执行目标函数。
}

after

我们在Function的原型上定义after的方法。

Function.prototype.after = function(fn,...argsSelf){
    this.apply(this,argsSelf);  //先执行目标函数。
    fn();                       //再执行fn
}

使用test调用这两个方法

test.before(function(){
    console.log(1);
})
test.after(function(){
    console.log(3);
})

我们来执行一下。 执行出来了。咦,我们发现2执行了两次,2就是所对应的目标函数(test),执行两次的原因是因为目标函数在before和after中都执行了一次。

这时候想到的方法是,如果能把before中的代码送到after里面,或者把after里面的方法送到before里面就可以实现只要目标函数执行一次的方法了。

方法推送

这里想到的是一种方法推送的方法(我自己取的名字),由于使用的是在Function的原型上编程,before和after都定义在Function.prototype上,所以所有的函数的原型链上都存在这两个方法。于是我们让before和after这两个方法返回一个函数。

Function.prototype.before = function(fn,...argsSelf) {
    let _self = this;       //对执行上下文做缓存
    return function (...args) {
        fn.apply(_self,args);       //先执行fn
        _self.apply(_self,argsSelf);    //再执行目标函数
    }
}
Function.prototype.after = function(fn,...argsSelf) {
    let _self = this;       //对执行上下文做缓存。
    return function (...args) {
        _self.apply(_self,argsSelf);    //先执行目标函数
        fn.apply(_self,args);       //再执行fn
    }
}
//执行
 test.before(function () {
    console.log(1);
}).after(function () {      
    console.log(3);
})()

这里可能会有点绕,before可能会好理解一下,通过test调用返回一个函数,而after方法则是通过before返回的函数调用的。也就是说,after里面的_self就是对应的before返回的方法,在after中执行_self.apply()就是执行的before返回的函数。

我们来测试一下。 1,2,3按顺序输出并且2只执行一次,符合我们的要求。

这里如果把before和after的调用顺序颠倒会是怎么??

test.after(function () {
    console.log(3);
}).before(function () {
    console.log(1);
})()

还是 1,2,3 可以。

到目前为止貌似已经达到了我们所要实现的功能,但是还并不是完美。

目标函数返回值

如果目标函数存在返回值的情况下,因为整个执行流程为:before->target->after 所以,目标函数的返回值可以在after中返回。

Function.prototype.before = function(fn,...argsSelf){
    var _self = this;
    return function(...args){
        fn.apply(_self,args);
        return _self.apply(_self,argsSelf); //返回目标函数所返回返回值,供后续使用
    } 
}
Function.prototype.after = function(fn,...argsSelf){
    var _self = this;
    return function(...args){
        var result = _self.apply(_self,...argsSelf);    //接受到上面一层的返回值。
        fn.apply(_self,args);
        return result;      //把返回值返回。
    }  
}
//在test函数中返回一个值
function test() {
    console.log(2);
    return 'test';
}

我们来执行一下。

成功了。

还有一个值得关注的地方就是,如果在before中返回了false,那么将不再进行目标函数和after方法的执行。

中段执行

中断执行用于before方法中出了差错等等。当before中返回false的时候,就中断后续方法的执行。

Function.prototype.before = function(fn,...argsSelf){
    var _self = this;
    return function(...args){
        if(fn.apply(_self,args) == false){  //判断before的回调是否返回false,如果返回把false传递下去
            return false;
        }
        return _self.apply(_self,argsSelf);
    } 
}
Function.prototype.after = function(fn,...argsSelf){
    var _self = this;
    return function(...args){
        var result = _self.apply(_self,...argsSelf);
        if(result == false){    //判断before返回的函数的返回值是否是false,如果是false,直接返回false中断执行,否则继续执行after中的回调
            return false;
        }
        fn.apply(_self,args);
        return result;
    }  
}

假设before返回false 假设before不返回false

到此,面向切面编程已经全部实现,回到最初的题目。

回到题目

比如说有这么一个函数,我们需要统计这个函数的执行时间是多少?

这时候我们有了上面的before和after方法就可以统计了

//test函数--执行这么一个for循环。
function test() {
    for(var i = 0; i < 100000000; i ++){}
}

//执行
test.before(function () {
    console.log(Date.now())
}).after(function () {
    console.log(Date.now())
})()

所得到的结果为:

如果想得到两个时间之间的话可以在Function上绑定一个变量。

test.before(function () {
    Function.firstTime = Date.now();
}).after(function () {
    console.log(Date.now()-Function.firstTime);
})();

执行一下: 成功!