俺的回收站

架构分析 解释编译原理
  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理
你相信么,在JavaScript只需一个函数5行代码即可实现完整的面向方面AOP编程功能。这5行代码的功能包括:
  • 无限层次的函数无害拦截
  • 函数执行前拦截
    • 检查函数的参数值
    • 重新设定函数的参数值
  • 函数执行后拦截
    • 检查函数执行后的返回结果
    • 重新设定函数的返回结果
虽然动态函数式语言的效率是一个存在的问题,但是对于它的高度灵活性,简直让人令人惊叹不已,剧赞。
这个小小的函数源自于和爱明兄的一次讨论:在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行。如果你清楚
的知道匿名函数的特点,那么这个函数,你就不难看懂,否则你就只能管用了。用法在注释的例子里,够简单吧。
/*
  @desc  inject the function
  @param aOrgFunc     the original function to be injected.
  @param aBeforeExec  this is called before the execution of the aOrgFunc.
                      you must return the arguments if you wanna modify the value of the aOrgFunc's arguments .
  @param aAtferExec   this is called after the execution of the aOrgFunc.
                      you must add a result argument at the last argument of the aAtferExec function if you wanna 
                      get the result value of the aOrgFunc.
                      you must return the result if you wanna modify the result value of the aOrgFunc .

  @Usage  Obj.prototype.aMethod = Inject(Obj.prototype.aMethod, aFunctionBeforeExec[, aFunctionAtferExec]);
  @author  Aimingoo&Riceball

  eg:
  var doTest = function (a) {return a};
  function beforeTest(a) { alert('before exec: a='+a); a += 3; return arguments;};
  function afterTest(a, result) { alert('after exec: a='+a+'; result='+result); return result+5;};
  
  doTest = Inject(doTest, beforeTest, afterTest);
  
  alert (doTest(2));
  the result should be 10.

*/
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函数如下,这下功能全了,具体代码也不过十多行:

/*
  @desc  inject the function
  @param aOrgFunc     the original function to be injected.
  @param aBeforeExec  this is called before the execution of the aOrgFunc.
                      you must return the arguments(new Arguments(arguments)) if you wanna modify the arguments value of the aOrgFunc.
                      it will stop the execution of the aOrgFunc if you return a value not an Arguments object nor a undefined value 
  @param aAtferExec   this is called after the execution of the aOrgFunc.
                      you must add a result argument at the last argument of the aAtferExec function if you wanna get the result value of the aOrgFunc.
                      you must add a isDenied argument following the result argument if you wanna know whether the aOrgFunc is executed.
                      you must return the result if you wanna modify the result value of the aOrgFunc .

  @Usage  Obj.prototype.Method = Inject(Obj.prototype.Method, aFunctionBeforeExec[, aFunctionAtferExec]);
  @version 1.1
  @author  Aimingoo&Riceball
  @history
    V1.0 -- fiest released.
    V1.1 -- 
      Supports to denie the aOrgFunc execution in aBeforeExec.
      Supports around in the aAtferExec, the aAtferExec be always executed even though denie the aOrgFunc execution in aBeforeExec.
        + isDenied argument to the aAtferExec function. notice the aAtferExec whether the aOrgFunc is executed

  eg:
  var doTest = function (a) {return a};
  function beforeTest(a) { alert('before exec: a='+a); a += 3; return new Arguments(arguments);};
  function afterTest(a, result, isDenied) { alert('after exec: a='+a+'; result='+result+';isDenied='+isDenied); return result+5;};
  
  doTest = Inject(doTest, beforeTest, afterTest);
  
  alert (doTest(2));
  the result should be 10.

*/
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, args);
      
if (Result instanceof Arguments) //(Result.constructor === Arguments)
        args = Result.value;
      
else if (isDenied = Result !== undefined)
        args.push(Result)
    }

    
!isDenied && args.push(aOrgFunc.apply(this, args)); //if (!isDenied) args.push(aOrgFunc.apply(this, args));

    
if (typeof(aAtferExec) == 'function')
      Result 
= aAtferExec.apply(this, args.concat(isDenied));
    
else 
      Result 
= undefined;

    
return (Result !== undefined ? Result : args.pop());
  }
}