在編寫js的時候,我們有時會遇到針對某種場景做處理,比如在方法開始的時候校驗參數,執行方法前檢查權限,或是刪除前給出確認提示等等。這些校驗方法、權限檢測、確認提示,規則可能都是相同的,在每個方法前去調用,顯得麻煩,而且不利於統一管理,於是我們想到了面向切面編程(AOP)。
1. 簡單AOP實現
簡單的AOP實現,就是在原函數執行的前后,增加運行before和after兩個增強方法,用這個新函數替換原函數,為此,我編寫了一個類似於構造器的函數(后文就稱它為構造器),代碼如下:
// originFun為原函數,before和after為增強方法 function constructor(originFun, before, after){ function _class(){ before.apply(this, arguments); originFun.apply(this, arguments); after.apply(this, arguments); } return _class; }
使用時,用構造器生成的新函數替換原函數即可,測試代碼如下:
// 加法運算,作為測試的原函數 function calcAdd(a,b){ console.log(a + "+" + b + "=" + (a + b)); return a+b; } // AOP增強 calcAdd = constructor(calcAdd, function(){console.log("我在原方法前執行")}, function(){console.log("我在原方法后執行")}); // 調用方法進行測試 calcAdd(2, 3);
打印在控制台的測試結果:
我在原方法前執行 2+3=5 我在原方法后執行
2. AOP工廠
在某些場景下,使用的增強方法是相同的,每次將增強方法作為參數傳遞有點麻煩,於是我做了一個工廠方法,把增強方法作為參數,這樣就可以生成不同的構造器,在不同場景調用不同的構造器即可:
// AOP工廠 var aopFactory = function(before, after){ // 構造方法,在原方法前后增加執行方法 function constructor(originFun){ function _class(){ var result; proxy.before(arguments); result = originFun.apply(this, arguments); proxy.after(arguments); return result; } return _class; } var proxy = { // 添加被代理方法,參數a為被代理方法,參數b為目標對象 add : function(a, b){ var funName; // 判斷參數a類型,可以為方法或方法名 if(typeof a == "function"){ funName = a.name; }else if(typeof a == "string"){ funName = a; }else{ return; } // 不傳對象時默認為window對象 b = b || window; if(typeof b == "object" && b[funName]){ // 替換原方法 b[funName] = constructor(b[funName]); } }, // 默認before為空方法 before : function(){}, // 默認after為空方法 after : function(){} } // 注入特定的前后處理方法 if(typeof before == "function"){ proxy.before = before; } if(typeof after == "function"){ proxy.after = after; } return proxy; }
測試代碼如下:

var printProxy, checkProxy; // 打印參數 function printArguments(){ var i, length; for(i=0, length=arguments.length; i<length; i++){ console.info("param" + (i + 1) + " = " + arguments[i]); } } // 驗證參數是否為數字 function checkNumber(){ var i, length; for(i=0, length=arguments.length; i<length; i++){ if(typeof arguments[i] != "number") console.error(arguments[i] + "不是數字"); } } // 將printArguments方法作為前置通知,生成打印參數的構造器 printProxy = aopFactory(printArguments); // 將checkNumber方法作為前置通知,生成驗證參數是否為數字的構造器 checkProxy = aopFactory(checkNumber); // 加法 function calcAdd(a,b){ console.log(a + "+" + b + "=" + (a + b)); return a+b; } // 減法 function calcMinus(a,b){ console.log(a + "-" + b + "=" + (a - b)); return a-b; } // 乘法 function calcMultiply(a,b){ console.log(a + "*" + b + "=" + (a * b)); return a*b; } // 除法 function calcDivide(a,b){ console.log(a + "/" + b + "=" + (a / b)); return a/b; } // 為加減法生成驗證參數是否為數字的代理方法 checkProxy.add(calcAdd); checkProxy.add(calcMinus); // 為乘除法生成打印參數的代理方法 printProxy.add(calcMultiply); printProxy.add(calcDivide); // 測試 calcAdd("4", 5); calcMinus(6, "a"); calcMultiply(4, 5); calcDivide(6, 3);
測試結果如下:

4不是數字 4+5=45 a不是數字 6-a=NaN param1 = 4 param2 = 5 4*5=20 param1 = 6 param2 = 3 6/3=2
3. 進一步優化AOP工廠
在before方法中,驗證到參數不正確,我們並不想讓方法繼續執行下去。我們可以讓before方法返回一個布爾值,作為停止執行的標志。在原方法執行前檢查before方法的返回值,判斷是否繼續往下執行。
另外,為每個方法生成代理都要調用一次add,這還不夠簡單,於是想到了正則表達式,通過循環,把所以滿足正則表達式的方法都進行增強。
根據以上兩點得到新AOP工廠方法:
// 優化后的AOP工廠 var aopFactory = function(before, after){ // 構造方法,在原方法前后增加執行方法 function constructor(originFun){ function _class(){ var result; result = proxy.before.apply(this,arguments); // 如果before方法返回false,則直接return不再往下執行 if(typeof result == "boolean" && !result){return;} result = originFun.apply(this, arguments); proxy.after.apply(this,arguments); return result; } return _class; } var proxy = { // 添加被代理方法,參數a為被代理方法,參數b為目標對象 add : function(a, b){ var funName, index; // 不傳對象時默認為window對象 b = b || window; if(typeof b != "object") return; // 判斷參數a類型,如果為正則表達式 if(typeof a == "object" && a.test){ // 替換所以滿足正則表達式的方法 for(index in b){ if(a.test(index)){ b[index] = constructor(b[index]); } } return; } // 判斷參數a類型,取出方法名 if(typeof a == "function"){ funName = a.name; }else if(typeof a == "string"){ funName = a; }else{ return; } // 如果方法存在,替換原方法 if(b[funName]){ b[funName] = constructor(b[funName]); } }, // 默認before為空方法 before : function(){}, // 默認after為空方法 after : function(){} } // 注入特定的前后處理方法 if(typeof before == "function"){ proxy.before = before; } if(typeof after == "function"){ proxy.after = after; } return proxy; }
測試代碼:

var checkProxy, myFunc; // 驗證參數是否為數字,是數字就打印,否則給出錯誤提示 function checkNumber(){ var i, length, flag = true; for(i=0, length=arguments.length; i<length; i++){ if(typeof arguments[i] != "number"){ console.error(arguments[i] + "不是數字"); flag = false; }else{ console.info("param" + (i + 1) + " = " + arguments[i]); } } return flag; } // 將checkNumber方法作為前置通知,生成驗證參數是否為數字的構造器 checkProxy = aopFactory(checkNumber); myFunc = { // 加法 calcAdd : function(a,b){ console.log(a + "+" + b + "=" + (a + b)); return a+b; }, // 減法 calcMinus : function (a,b){ console.log(a + "-" + b + "=" + (a - b)); return a-b; }, // 計算冪 power : function(a,b){ console.log(a + "^" + b + "=" + Math.pow(a, b)); return Math.pow(a, b); } } // 對myFunc對象中所有"calc"開頭的方法進行增強 checkProxy.add(/^(calc).*/, myFunc); // 測試 console.log("calcAdd--------------"); myFunc.calcAdd(4, 5); console.log("calcMinus--------------"); myFunc.calcMinus(6, "a"); console.log("power--------------"); myFunc.power(4, 3);
測試結果:

calcAdd-------------- param1 = 4 param2 = 5 4+5=9 calcMinus-------------- param1 = 6 a不是數字 power-------------- 4^3=64
測試對myFunc中所有"calc"開頭的方法進行增強,從結果可以看到,加減法運算前都打印了參數,而且減法運算由於參數不正確,並沒有執行。冪運算不是我們希望增強的方法,它的參數沒有被打印出來。
對於增強方法中有回調函數的情況,我想到的是把原函數及其參數作為回調函數的參數進行傳遞,沒有發現什么更好方法,就不細說了。
最后,文章中有什么問題,或是大家有什么好的想法,記得告訴我哦!