俺的垃圾箱

架构分析 解释编译原理
posts - 32, comments - 228, trackbacks - 10, articles - 1
  博客园 :: 首页 :: 新随笔 :: 联系 :: 订阅 订阅 :: 管理
你相信么,在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());
  }
}




Feedback

#1楼    回复  引用    

2007-09-02 17:20 by 小鬼 [未注册用户]
什么是AOP?

#2楼    回复  引用  查看    

2007-09-02 17:58 by 布尔      
直接在Inject中写
aOrgFunC=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();
}

#3楼    回复  引用    

2007-09-02 18:28 by SS [未注册用户]
我一行代码就实现一个网游.

start wow.exe

#4楼    回复  引用  查看    

2007-09-02 19:09 by 大石头      
楼主的js功力不浅嘛,呵呵。
这里我冒昧写点注释。

function Inject( aOrgFunc, aBeforeExec, aAtferExec ) {
return function() {
//如果指定的“调用前函数”(aBeforeExec)是function类型,则在当前对象this中,以调用当前函数的参数arguments直接调用“调用前函数”,
//“调用前函数”的返回值覆盖掉当前参数对象arguments,如果“调用前函数”没有返回值,当然还使用当前参数对象作为参数啦。
if (typeof(aBeforeExec) == 'function') arguments = aBeforeExec.apply(this, arguments) || arguments;
//convert arguments object to array
//[].slice.call(arguments)写得太玄了,args=arguments.slice()就可以了
var Result, args = [].slice.call(arguments);
//哈哈,开始调用原始函数啦。结果放在args数组的最后
args.push(aOrgFunc.apply(this, args));
//执行“调用后的函数”,参数就是“调用前函数”的返回和原始函数的返回值
if (typeof(aAtferExec) == 'function') Result = aAtferExec.apply(this, args);
//如果“调用后函数”没有返回值,直接使用原始函数的返回值啦,也就是args.pop()啦。
return (typeof(Result) != 'undefined')?Result:args.pop();
}
}

#5楼    回复  引用  查看    

2007-09-02 20:22 by ppchen      
收藏下。。。

#6楼    回复  引用  查看    

2007-09-02 21:18 by Phinecos(洞庭散人)      
这个就是AOP?

#7楼    回复  引用    

2007-09-02 21:41 by DarkAndLight [未注册用户]
偶去补JS课去,看的发晕~~~~

#8楼    回复  引用  查看    

2007-09-02 21:51 by Jeffrey Zhao      
JS非常灵活,本来就是一种动态语言,这些都是它天生的能力,统统不在话下的。

#9楼    回复  引用  查看    

2007-09-02 22:31 by 浪子      
aBeforeExec方法应该是判断是否可以执行的功能比较合适。
:)

#10楼    回复  引用  查看    

2007-09-02 23:19 by Cat Chen      
这些都是JavaScript天生具备的能力了,只是人们需要一步一步将它挖掘出来。

#11楼    回复  引用  查看    

2007-09-03 00:19 by Leepy      
能举个实例说明一下该段代码的作用么?

#12楼    回复  引用  查看    

2007-09-03 09:53 by 深蓝      
太深奥了.完全没有看懂.楼主的JS太牛了

#13楼    回复  引用    

2007-09-03 10:59 by lion [未注册用户]
楼主的标题不正确
刚才 大石头 已经解释了语句的意思,如果只用行来说的话,去掉回车/换行/Tab写在一行里也可以

在我看来,你这篇post的意思就是在解释这一句话"虽然动态函数式语言的效率是一个存在的问题,但是对于它的高度灵活性,简直让人令人惊叹不已"

#14楼    回复  引用  查看    

2007-09-03 11:08 by ╃小〥斌╄      
如果能搭配应用场景就好了。。。

#15楼    回复  引用  查看    

2007-09-03 11:24 by Wisdom-zh      
传函数进去显得罗唆, 而且对返回值的处理也不好.

#16楼    回复  引用    

2007-09-03 11:26 by srykdm [未注册用户]
//[].slice.call(arguments)写得太玄了,args=arguments.slice()就可以了。
不能这样修改,arguments是对象(Object),没有slice成员函数。 而[].slice.call(arguments),类似于给arguments加了一个slice函数。

#17楼 [楼主]   回复  引用  查看    

2007-09-03 13:16 by Riceball LEE      
to 布尔
大胆假设是好事,但是如果不能小心求证就成了坏事了。
js函数参数为值参,没有变参,你这样是行不通的。

to 大石头
谢谢你的注释,不过俺不算js高手,只是在研究动态函数式语言引擎的执行方式,爱民兄才是js高手,正在写js动态函数式语言的书呢。
另:sykdm说得对,只有老版本的js的arguments才是Array,而自从js1.3版本开始就不是数组对象了。

to 浪子
如果不想执行原函数,不能通过在BeforeExec中 throw 一个异常实现么?如果非要如此,也可以,不过又要增加代码量了。

#18楼    回复  引用    

2007-09-03 13:53 by A.Z [未注册用户]
如果这是一个递归的func呢?

#19楼    回复  引用  查看    

2007-09-03 14:07 by ╃小〥斌╄      
我觉得
FCKEditingArea.prototype.Start = function( func ) {
return function( html ) {
html = html.replace( FCKRegexLib.HeadCloser, sHeadExtra + '$&' ) ;
return func.apply(this, arguments);
}
}

这样倒是挺好读的。。

#20楼    回复  引用    

2007-09-03 15:32 by yok [未注册用户]
prototype1.6已经实现aop了

#21楼    回复  引用  查看    

2007-09-03 16:55 by 浪子      
@Riceball LEE
其实ext里面很早就是这些封装了,写得挺不错的,可以参考参考。

#22楼    回复  引用    

2007-09-03 18:16 by 箫音 [未注册用户]
有点变态用法!^_^

#23楼    回复  引用  查看    

2007-09-03 22:49 by 蛙蛙池塘      
动态语言就是灵活,呵呵,.net一个密封类的公开方法执行的时候可以在前面和后面或者抛出异常的时候执行自定义代码吗?

#24楼    回复  引用    

2007-09-04 00:45 by 壁虎 [未注册用户]
js完全不行,看不明白。
有什么好点js的入门书或教程的,推荐个。

#25楼    回复  引用    

2007-09-04 13:44 by 一行?? [未注册用户]
你为何不把所有代码写成一行??用分号隔开????
你为何不把所有代码写成一行??用分号隔开????
你为何不把所有代码写成一行??用分号隔开????
你为何不把所有代码写成一行??用分号隔开????
你为何不把所有代码写成一行??用分号隔开????
你为何不把所有代码写成一行??用分号隔开????

#26楼 [楼主]   回复  引用  查看    

2007-09-04 17:40 by Riceball LEE      
to 壁虎
俺的方法也许不适合您。
我一般在网上用google找一本js语言参考手册,
然后猜测它的语法和结果,在上机验证。



to 一行
小朋友要讲卫生哟,请勿随地大小便嘛,污染环境是不好滴。