日期:2014-05-16  浏览次数:20466 次

javascript 实现AOP

你相信么,在JavaScript只需一个函数5行代码即可实现完整的面向方面AOP编程功能。这5行代码的功能包括:

1、无限层次的函数无害拦截

2、函数执行前拦截
3、检查函数的参数值
4、重新设定函数的参数值
5、函数执行后拦截
6、检查函数执行后的返回结果
7、重新设定函数的返回结果

虽然动态函数式语言的效率是一个存在的问题,但是对于它的高度灵活性,简直让人令人惊叹不已,剧赞。
这个小小的函数源自于和爱明兄的一次讨论:在javascript中不修改源代码的形式扩充新功能的完美方式。
至于为啥要不直接修改别人源代码库,你想,当你辛苦修改后,人家发布了一个新版本,你是不是又要重新
修改一次?如果修改的地方少还好,多了的话,嘿嘿!所以,不是你的东西,最好不要动,除非你确定要自
己完全维护所有的代码(如果你实在太闲)。

为了给开发人员一个榜样,俺不得不写一个patch样例,最开始俺写的patch方式是这样滴:
FCKEditingArea.prototype.FckMediaStart = FCKEditingArea.prototype.Start;
FCKEditingArea.prototype.Start =? function( html, secondCall )
{
? var sHeadExtra = '<link href="' + FCKConfig.PluginsPath + 'media/css/fck_media.css" rel="stylesheet" type="text/css" _fcktemp="true" />' ;
? html = html.replace( FCKRegexLib.HeadCloser, sHeadExtra + '$&' ) ;
? return this.FckMediaStart(html, secondCall);
}

俺觉得这种补丁方式是不太让我满意的

爱民兄提到他喜欢这样patch方式:
FCKEditingArea.prototype.Start = function( func ) {
? return function( html ) {
??? html = html.replace( FCKRegexLib.HeadCloser, sHeadExtra + '$&' ) ;
??? return func.apply(this, arguments);
? }
}(FCKEditingArea.prototype.Start);

这种方式的确不存在漏洞了,perfect,但是却把我彻底弄懵了,看得我头大啊!琢磨了好久才算明白过来。
不过从管理角度考虑,如果程序中到处都是这样的方式,那么将大大降低程序的可读性,增加维护代码的成本
(ps,要开发维护这样的代码,你不得不请JS高级程序员才行)。
为了限制匿名函数的滥用导致的可读性下降,俺写了一个Inject()函数的雏形(将匿名函数的使用限制在其中),
经过爱民兄修改,然后又是讨论,然后我们又修改,如此反复,达到这个最终版本(也许还不是,谁知道呢)。


好了,荣誉归于爱民,臭鸡蛋归于俺,闲话少说,看看代码吧,不算上注释,这个Inject函数正好5行。如果你清楚
的知道匿名函数的特点,那么这个函数,你就不难看懂,否则你就只能管用了。用法在注释的例子里,够简单吧。

function Inject( aOrgFunc, aBeforeExec, aAtferExec ) {
? return function() {
??? if (typeof(aBeforeExec) == 'function') arguments = aBeforeExec.apply(this, arguments) || arguments;
??? //convert arguments object to array
??? var Result, args = [].slice.call(arguments);
??? args.push(aOrgFunc.apply(this, args));
??? if (typeof(aAtferExec) == 'function') Result = aAtferExec.apply(this, args);
??? return (typeof(Result) != 'undefined')?Result:args.pop();
? }
}

使用新的Inject方式的代码如下:
FCKMediaProcessor.EditingArea_StartBefore = function ( html, secondCall )
{
? var sHeadExtra = '<link href="' + FCKConfig.PluginsPath + 'media/css/fck_media.css" rel="stylesheet" type="text/css" _fcktemp="true" />' ;
? html = html.replace( FCKRegexLib.HeadCloser, sHeadExtra + '$&' ) ;
? //在执行前修改了参数值,所以返回修改后的参数
? return arguments;
}
FCKEditingArea.prototype.Start =? Inject(FCKEditingArea.prototype.Start, FCKMediaProcessor.EditingArea_StartBefore);


ok,下面将继续增强 Inject的功能,将会把浪子提到的控制原函数是否执行加上。
本增强版本和上面的Inject函数的版本使用上略有差别,使用规范如下:

执行前调用(BeforeExec)
如果修改了参数值,必须这样返回修改的参数: return new Arguments(arguments);
如果没有返回值或者返回undefined那么正常执行,返回其它值表明不执行原函数,该值作为替代的原函数返回值。
执行后调用(AfterExec)
如果希望取得函数的返回值,必须在原参数表后面增加一个参数: result
如果希望知道原函数是否被执行,那么必须在 result参数的后面再增加一个参数: isDenied 该值为真表明没有执行原函数
如果希望修改函数的返回值,那么你只要简单的把修改后的值返回即可。

示例:假设我们要拦截一个 doTest 函数,修改它的参数值a,在原参数值上+1,并在它的函数返回值上也+1:
//定义测试函数 doTest
var doTest = function (a) {
? alert('dotest 运行中');
? return a;
};

function beforeTest(a) {
? alert('执行前参数: a='+a);
? a += 1;
? return new Arguments(arguments);
}

function afterTest(a, result, isDenied) {
//这里不会体现出参数a的改变,如果原函数改变了参数a。因为在js中所有参数都是值参。
? alert('执行后参数: a='+a+'; result='+result+';isDenied='+isDenied);
? return result+1;
}
?
//注入该doTest函数
doTest = Inject(doTest, beforeTest, afterTest);

//执行 doTest并显示结果
alert (doTest(2));

和爱民兄谈论后,修订后的新的Inject函数如下,这下功能全了,具体代码也不过十多行:


function Arguments(args) {
? //convert arguments object to array
? this.value = [].slice.call(args);
}
function Inject( aOrgFunc, aBeforeExec, aAtferExec ) {
? return function() {
??? var Result, isDenied=false, args=[].slice.call(arguments);
??? if (typeof(aBeforeExec) == 'function') {
????? Result = aBeforeExec.apply(this