Flash ActionScript3 殿堂之路 第四章ActionScript3中的函数及高级使用技巧

来源:互联网 发布:市民刘先生 知乎 编辑:程序博客网 时间:2024/05/22 05:28

函数是什么?
       函数从外观上看,是一个语句块,包含着至少一条或数条语句;从逻辑上看,它是可以执行某个目标任务的代码块。它可以接受外部传入的对象或值,还可以返回操作的结果。
       函数的准确定义是:“函数”(Function)是执行特殊任务并可以在程序中重要的代码块。ActionScript3中函数分为两类;方法(Method)和函数闭包(Function closures)。如果将函数定义为类定义的一部分或者将它与某个对象绑定,则该函数成为方法。

本章导读
     本章介绍ActionScript3函数新增的功能和独有的性质。本章带星号部分比较难,有些涉及到了ActionScript3类结构的知识。
对初学者而言,掌握4.2节及其之前的内容即可。
     对熟悉ActionScript2的读者来说,本章要全部仔细阅读,ActionScript3的函数不论是用法还是底层实现细节上都有重大变化。尤其对4.1.3节、4.3节要仔细阅读。
     对于熟悉其他语言的读者,ActionScript3的函数概念虽然与其他语言相同,但其本质和很多特性是独有的。请仔细阅读本章所有内容。尤其注意4.2.1节、4.3节。
     4.4节讲述了一些ActionScript3独有的函数运用高级技巧,请详细阅读并实践。

     4.1 两种定义函数方法函数有两种定义方法。一种是非常熟悉的函数语句(Function statement)声明法:另外一种是ActionScript特有的函数表达式(Function expression)声明法。

4.1.1函数语句定义法    
函数语句定义法与其他语言中的类似,使用function关键字来声明,格式如下:

  1. function 函数名(参数1:参数类型,参数2:参数类型…):返回值类型 {

  2. //函数内部语句
  3. }
  4. //例子:

  5. function testAdd(a:int,b:int):int {

  6. return a+b;
  7. }
复制代码
4.1.2函数表达式定义法函数表达式定义法是ActionScript特有的一种定义方法。定义格式如下,请读者与上文比较。
  1. var函数名:Function = function(参数1:参数类型,参数2:参数类型…):返回值类型{

  2. //函数内部语句
  3. }
  4. //例子:

  5. var testAdd:Function = function (a:int,b:int):int {

  6. return a+b;
  7. }
复制代码
进阶知识



“=”号右边的内容function后面没有跟随函数名,相当于创建了一个匿名函数对象,并将引用赋值给了左边的函数型变量。


4.1.3 *两种定义法的区别和选择
      这两种定义法定义的函数在一般使用中不会感觉到有什么不同。平时,应当尽量使用函数语句定义法。这种定义方式更加标准,也很简捷。
      在定义位置上,函数语句定义法定义的函数在编译时会被提升(hoisting)到最前面,可函数表达式定义法只能先定义再使用,只有定义后面的语句才可以成功调用。如下列,testA()和testB()都是在最后面定义的,testB()能执行成功,但是testA()由于使用的是函数表达式定义法,就不行。要想成功执行testA(),必须要把testA()移到testA定义后面。
  1. testA();//
  2. testB();
  3. var testA:Function = function():void {trace(“A”)};
  4. function testB():void { trace (“B”)}
复制代码
除此以外,函数语句定义法和函数表达式定义法还体现在函数体中this关键字的记忆上使用函数语句定义法,则this关键字牢牢指向当前函数定义的域;而若使用函数表达式定义法,则随着函数附着的对象不同,this关键字也随之改变。下文还会提到,函数额apply()和call()只能改变函数表达式定义的函数this指向,而不能改变函数语句定义法定义的函数。
      见示例4-1:下例的代码写在时间轴上,因此当前函数定义域为时间轴——MainTimeLine对象。

      示例4-1比较this在函数语句定义法和函数表达式定义法中的不同
  1. var num:int = 3;
  2. function testThisA(){

  3. trace (num);

  4. trace(this.num);

  5. trace(this);
  6. }
  7. var testThisB:Function = function() {

  8. trace (num);

  9. trace(this.num);

  10. trace(this)
  11. }
  12. var obj:Object = {num:300};
  13. obj.testB
  14. =
  15. testThisB;
  16. obj.testA
  17. =
  18. testThisA;

  19. testThisA();
  20. obj.testA();
  21. testThisA.apply(obj);
  22. //用apply试图将testThisA()的this关键字指向绑定到obj上,发现输出没改变
  23. /*以上3个输出的都是:
  24. 3
  25. 3
  26. [object MainTimeline]
  27. */
  28. testThisB();
  29. /*输出:
  30. 3
  31. 3
  32. [object MainTimeline]
  33. */
  34. obj.testB();
  35. /*输出:
  36. 3
  37. 300
  38. [object Object]
  39. */
  40. testThisB.apply(obj);
  41. //用apply试图将testThisB()的this关键字指向绑定到obj上,发现输出改变,绑定成功
  42. /*输出:
  43. 3
  44. 300
  45. [object Object]
  46. */
复制代码
函数语句与函数表达式之间还有两个细微的区别:第一个区别体现在内存管理和垃圾回收方面。因为,函数表达式不像对象那样独立存在,它相当于一个匿名函数。当持有这个函数引用的数组或对象脱离作用域或由于其他原因不再可用,那么将没有任何方法再访问匿名函数。此时,这匿名函数对象引用计数为零,符合垃圾回收条件,这意味着可能被回收。
     除此以外,使用匿名语句法定义的函数和使用函数表达式定义法定义的函数还有一个本质的区别。参见4.3节“*函数的本质”。

4.2参数
       ActionScript3中对函数参数增加了一些限制,也多了一些功能。增加的限制在于,现在的函数一旦定义好参数类型和个数,那么在调用时参数个数和类型必须相符,否则编译报错。如果使用了新增功能:参数默认值或者…(rest)关键字,那么才可用增加传入参数的灵活性。
接触任何一门语言的函数,首先要问的问题就是参数到底是传引用还是传值。这两种方式对代码编写有很大的影响。
4.2.1按值还是按引用来传入参数
    按值传入,那么参数对象会复制一份副本供函数内部操作,参数对象本身不会受影响;按引用传入,则不复制副本,函数内操作参数对象的引用,那么会对改变参数对象的状态。
    在ActionScript3中,所有的参数都是按引用传入的。只不过,基元数据类型是不变对象,传引用和传值的效果一样。所以,如果参数是基元数据类型,那么可用看成是传值;如果参数不是基元数据类型,那么就是传引用。见示例4-2。
  1. function test(valuePara:int, referencePara:Array):void{

  2. valuePara = 100;

  3. referencePara.push(100);
  4. }

  5. var a:int = 5;
  6. var b:Array = [1,2,3];
  7. test(a,b);
  8. trace (a);
  9. //输出:5,a没有变成100,保持不变
  10. trace (b);
  11. //输出:1,2,3,100
  12. 被改变
复制代码
4.2.2设置默认参数
       在ActionScript3中可用设置函数的默认参数。如果调用函数时,没有写明参数,那么会调用该参数默认值代替。格式如下:
  1. function (参数1:类型=默认值,参数2:类型=默认值):返回类型{…}
复制代码
例子如下:
  1. function test(a:int = 3, b:int = 2 ,c:int = 1):void {

  2. trace (a+b+c,a,b,c);
  3. }
  4. test();
  5. //输出:6 3 2 1
  6. 本处没有传参数,全部使用了默认值
  7. test(9);
  8. //输出:12 9 2 1
  9. 本处传1个参数,使用了后两个参数默认值
  10. test(2,9);
  11. //输出:12 2 9 1
  12. 使用了最后一个参数默认值
  13. test(1,2,3);
  14. //输出:6 1 2 3
  15. 参数齐全,没有使用默认值
复制代码
4.2.3访问参数信息和…(rest)关键字
       在函数中传入的参数都被保留在了函数自动生成的一个arguments对象中。arguments如同一个数组,按参数定义的顺序保存者传入的参数。可用使用arguments[0]、arguments[1]访问传入的第一个参数、第二个参数,以此类推。
       它有一个属性length表示当前传入参数的数目;还有一个属性callee持有指向当前函数的引用,常常用来创建递归。
       见下例,该函数将传入的参数相加,如果大于0,则各减1,再用arguments.callee()递归。
  1. function test (a:int = 3, b:int = 2 , c:int =1):void {

  2. trace (“参数长度:” + arguments.length);

  3. trace (a+b+c);

  4. if (a+b+c >0) arguments.callee(a-1,b-1,c-1);
  5. }
  6. test();
  7. /*输出:
  8. 参数长度:0 (因为第一次使用的是test(),没有传入任何参数,所以为0)
  9. 6
  10. 参数长度:3 (这是因为argements.callee()填满了3个参数,因此是3)
  11. 3
  12. 参数长度:3
  13. */
复制代码
在ActionScript2中可用无视一个函数的定义,传入任意多的参数。但是在ActionScript3中要严格遵守函数定义,否则就是非法的。为了提供这种传入任意参数的灵活性,ActionScript3提供了一个新的关键字…(rest)。只要在参数中定义了…(rest),那么就可用接受任意多的参数。这些参数,以数组形式保留在rest中。rest只是推荐的命名,我们可用改成其他的名字,比如paras。
      另外要注意,一旦使用了…(rest)关键字,那么arguments就不能再使用了。
      看下例中的用法,见示例4-3。
      示例4-3 新关键字…(rest)用法示例
  1. function testA(…paras):void {

  2. trace (“参数长度:” + paras.length);

  3. for (var i in paras) trace (paras);
  4. }

  5. testA(1,[2,3],”String type”);
  6. /*输出:
  7. 参数长度:3
  8. 1
  9. 2,3
  10. String type
  11. */

  12. function testB(firstPara:String,…args):void {

  13. trace (arg.length);

  14. for (var i in args) trace (firstPara + “:”+ args);
  15. }
  16. testB(“B test”, [100,200],1000, “Foo”);
  17. /*输出:
  18. B test:100,200
  19. B test:1000
  20. B test:Foo
  21. */
  22. testB(2,3); //由于第一个参数是int型,没有符合firstPara定义,所以报错1067不成功
复制代码
4.3 *函数的本质函数究竟是什么?
       我们习惯了函数的存在,就像习惯了我们呼吸的空气却不去探究它的本质。看起来,似乎函数和Number、Boolean、String一样都是ActionScript本来就有的类型。实际上,函数是一种对象,在ActionScript3中,一切皆对象。包括原始数据类型也是Object,只不过是特殊的不变对象(immutable objects)类型。
       那么,函数本质上到底是怎样的一种Object?与其他编程语言不同,在ActionScript3中,函数本身是一个Function类型的对象,可以有独立的属性甚至方法。
       函数一旦执行,一个特殊的对象就建立了。我们称它为“Active Object”,它含有以上的属性和本地变量。函数的本质是这种特殊的Active Object。这个对象我们是不可访问的,属于内建的机制。同时,每个函数都有一个内置的范围链(scope chain),这时也将被建立,以使Flash Player来检查所有的声明。函数可以层层嵌套,范围链也是如此。最大的范围链当然是全局的范围链了,包括所有的全局变量和函数。

与ActionScript2、ActionScript1对比
但在ActionScript3与ActionScript2中,函数在底层实现上有很多不同之处。请仔细阅读本节。


      笔者研究发现,在ActionScript3中,使用函数语句定义法定义的函数,与使用函数表达式定义法定义的函数有诸多不同之处。其根本在于底层实现的不同。下面来一一介绍。

4.3.1函数语句定义法定义的函数对象本质
     先看以下代码,aFunc是一个使用函数语句定义法定义的函数:
  1. import flash.utils.getQualifiedClassName;
  2. trace (getQualifiedClassName(aFunc));
  3. //输出:builtin.as$0::MethodClosure
  4. trace (getQualifiedSuperclassName (aFunc));
  5. //输出:Function
  6. trace (aFunc is Function); //输出:true
  7. trace (aFunc is Object);
  8. //输出:true
  9. function aFunc() {

  10. trace (“This is afunc!Excuted!”);
  11. }
复制代码
第一行输出告诉我们aFunc的类型是一个内置的MethodClosure类型对象;第二行输出告诉我们这个神秘的MethodClosure类的父类是Function;第三行确认aFunc是Function类型的对象;第四行更加有意思,它告诉我们aFunc是一个Object。因为Function类是直接继承自Object的。
        但要注意,builtin.as$0::MethodClosure这个不公开的类虽然是Function的子类,但是它并不是动态类的,因此一些高级函数技巧不能用于使用函数语句定义法定义的函数。

4.3.2函数表达式定义法定义的函数对象本质
        看以下代码,aFunc是一个使用函数表达式定义法定义的函数:
  1. import flash.utils.getQualifiedClassName;
  2. var aFunc:Function = function() {

  3. trace (“This is bFunc!Excuted!”);
  4. }

  5. trace (getQualifiedClassName (aFunc));
  6. //输出:Function-1
  7. trace
  8. (aFunc is Function); //输出:true
  9. trace
  10. (aFunc is Object);
  11. //输出:true
复制代码
使用函数表达式定义法定义的aFunc的类型不再是MethodClosure,而是一个Function-1类型。这个Function-1是编译时生成的Function的子类。如果多使用函数表达式定义法定义几个函数对象,那么编译时会生成Function-2、Function-3等新的子类。
这些子类和MethodClosure不同,都是动态类。因此,函数表达式定义的函数可以使用4.4.4节、4.4.5节所介绍的高级函数运用技巧。

4.4 *函数高级使用技巧
      知道函数本身也是一种对象,会给我带来极大的便利和编程思维的改变。下面共享几个笔者使用的技巧。

4.4.1技巧一:代理函数对象
       这是最简单的运用。设立一个代理函数对象,根据条件的不同,将它指向不同的函数,可实现动态改变(即运行时改变)。相信有经验的程序员都了解动态改变函数的便利性。而且在于ActionScript提供了这种便利,运用这个特性可以衍生大量技巧。见示例4-4。
  1. var kingdaFunc:Function;
  2. var sex:String = “male”;
  3. if (sex == “male”) {

  4. kingdaFunc = maleFunc;
  5. } else {

  6. kingdaFunc = famaleFunc;
  7. }
  8. kingdaFunc();
  9. //输出:I am a boy
  10. function maleFunc() {

  11. trace (“I am a boy”);
  12. }
  13. function femaleFunc() {

  14. trace (“I am a girl”);
  15. }
复制代码
4.4.2技巧二:建立函数执行队列
比如说,我有一个对象,我想根据不同的情况对它进行一系列的操作。但是有时需要所有的操作,有时有只需要一部分的操作。那么这个较高级的技巧,就能保证代码的高度重用性和简捷。见示例4-5。

示例4-5 建立函数执行队列

  1. var funcAry:Array = new Array();

  2. //将需要的操作步骤加入队列
  3. funcAry.push(aFunc);
  4. funcAry.push(bFunc);
  5. funcAry.push(cFunc);


  6. //供操作的对象
  7. var originObject:Object = new Object();

  8. //需要执行几步由execQueue这个参数决定,在实际工程运用中这个数可能是动态决定的
  9. var execQueue:Number = funcAry.length;

  10. //核心步骤:函数队列执行。实际运用中可以把它包装成一个函数,或者一个类的实例
  11. for (var i:Number =0; i<funcAry.length; i++) {
  12. funcAry (originObject);
  13. }

  14. //trace出执行操作后的originObject里面的内容
  15. for (var j in originObject) {

  16. trace (j + “:” + originObject[j]);
  17. }

  18. //操作步骤a,b,c
  19. function aFunc(eO:Object) {

  20. e0.aFuncExected = true;

  21. trace (“aFunc()”);
  22. }
  23. function bFunc(eO:Object) {

  24. eO.aFuncExected = true;

  25. trace (“bFunc()”);
  26. }
  27. function bFunc(eO:Object) {

  28. eO.aFuncExected = true;

  29. trace (“cFunc()”);
  30. }
复制代码
输出内容为:
  1. aFunc()
  2. bFunc()
  3. cFunc()
  4. aFuncExected:true
  5. bFuncExected:true
  6. cFuncExected:true
复制代码
前3行表明a、b、c3个函数按顺序执行了。后3行表明orginObject确定经过了3步操作,多了3个为true的属性。
    笔者提醒:技巧可以再延伸!可以通过一个函数来管理队列里面各个元素的位置,达到改变操作函数的顺序。比如,通过一个数组来安排调用顺序。以下代码,紧接着上文的代码例子。
  1. var operationAry:Array = [2,1,0];
  2. for (var k:Number = 0; k<operationAry.length;k++) {
  3.   funcAry[operationAry[k]](originObject);
  4. }
  5. /*输出:
  6. cFunc()
  7. bFunc()
  8. aFunc()
  9. */
复制代码
这样函数就通过2、1、0这样的倒序来执行操作。
这个技巧还有很多可以延伸的地方,比如说动态控制操作函数的参数多等,甚至可以通过外部数据(如XML)运行时改变函数执行顺序。大家可以自己研究扩展。
4.4.3技巧三:利用函数返回
      函数有一个proxyObject对象,我们希望根据proxy对象的内容来确定一个方法,处理myObject对象。
      当proxyObject是字符串时,我们又希望根据字符串的内容来确定返回不同的函数(或方法)。这些函数参数和类型是不完全相同的。有的可能是一个参数,有的可能是多个参数,不同类型。
      那么传统的解决方法——在函数内部调用其他函数——就显得力不从心。即使解决,也不如笔者下面这个chooseFuncBy()函数简捷。缺点当然有,那就是比较灵活,使用者一定要清楚地管理好每个目标函数和判断逻辑。因为这种灵活的编程方式编译器无法检查,是不能查出类型不匹配这种错误的。初学者慎用。见示例4-6。

示例4-6 利用函数返回函数

  1. //通过A调用只有一个参数的aFunc();
  2. chooseFuncBy (“A”) (“A func has only one parameter.”);
  3. //输出:aFunc(): A func has only one parameter.

  4. //通过B调用有两个参数的bFunc();
  5. chooseFuncBy(“B”) (“B func has two parameters.”,”No.2 parameter”);
  6. //输出:bFunc():B func has two parameters.one more Parameter:No.2 parameter

  7. //字符串不符,默认函数
  8. chooseFuncBy(“wu lala”)(“I choose A function”);
  9. //输出:Welcome to Kingda.org!My blog

  10. var withObj:Object = new Object();
  11. var myObj:Object = 
  12. {name:”黑羽”,blog:”http://www.kingda.org”, hobby:”Starcraft”};
  13. chooseFuncBy(withObj)(myObj);
  14. /*输出:
  15. objectFunc();
  16. name:黑羽
  17. blog:http://www.kingda.org
  18. hobby:Strarcraft
  19. */

  20. function chooseFuncBy(input:*):Function {

  21. //运用一:利用参数的种类来确定返回的函数
  22. if (!(input is String)) {

  23. return objectFunc;
  24. }

  25. //运用二:根据参数内容来返回函数
  26. switch (input) {
  27. case “A”:
  28. return aFunc;
  29. case “B”:
  30. return bFunc;
  31. default:
  32. return kingdaFunc;
  33. }
  34. //…更多延伸运用:利用参数个数、is确定不同Class的实例来选择函数,等等
  35. }
复制代码
  1. function aFunc(nS:String) :void {

  2. trace (“aFunc():” + nS);
  3. }

  4. function bFunc(nS:String, nP:String) :void {

  5. trace (“bFunc():” + nS + “one more Parameter:”+ nP);
  6. }

  7. function kingdaFunc(…rest) :void {


  8. trace (“Welcome to Kingda.org! My blog”);
  9. }

  10. function objectFunc(kingdaObj:Object) :void {

  11. trace (“objectFunc():”);

  12. for (var i in kingdaObj) {

  13. trace (i + “:” + kingdaObj);
  14. }
  15. }
复制代码
4.4.4技巧四:函数动态添加实例属性

    如4.3节所说,使用函数表达式定义的函数也是一个动态类对象。因此,使用函数表达式定义的函数可以动态添加属性和方法。
    注意,技巧四和技巧五完全可以用面向对象的方法来解决。从面向对象的角度来说,频繁使用技巧四和技巧五不是一个好的编程习惯。虽然很方便,但由于动态属性的使用使得编译器无法进行类型检查,也使得在复杂代码中的犯错可能性增加,查错难度增大。技巧四和技巧五只是运用在一些特殊的场合,在面向过程的编程中使用稍多一些。
下面来介绍技巧的运用。
    例如,利用函数动态属性来计算函数调用次数。当然,你完全可以用这个技巧来干更多更有用的事,笔者只是抛砖引玉做个小例子。
一个游戏中有开火函数shot()。我想知道总共开火了多少次,那么可以添加一个shot函数的动态属性times。注意,引用shot函数动态属性时,只能使用“函数名+数组访问符+属性名”来访问,例:shot[‘times’]。见示例4-7。

示例4-7函数动态添加实例属性

  1. var shot:Function = function ():void {

  2. shot[‘times’] ++;

  3. trace (“Shot():times:”+ shot[‘times’]);

  4. //.. 可以写些其他代码放在这儿
  5. }

  6. shot[‘times’] = 0;
  7. //初始化times
  8. shot();
  9. //输出:Shot():times:1
  10. shot();
  11. //输出:Shot():times:2
  12. shot();
  13. //输出:Shot():times:3
复制代码
4.4.5技巧五:函数对象动态添加实例方法
      技巧五就是添加方法了,更有趣一些。函数这么一摆弄之后,函数对象成了“二不像”:不像普通类实例,也不像一个函数。这个技巧可以让我们的函数变得很强大,也会让它更复杂更难以管理。本技巧即使在面向过程编程中,也只建议在小范围内少了运用,不赞成大范围大规模地使用。
见示例4-8,本例中一旦发现shot()函数开火超过3次,就会自动调reload,将开火次数重置为0。

示例4-8 函数对象动态加实例方法

  1. var shot:Function = function():void {

  2. shot[‘times’] ++;

  3. trace (“Shot():times:”+ shot[‘times’]);

  4. shot[‘reload’]();
  5. //shot的其他代码放在这儿
  6. }
  7. shot[‘times’] = 0;

  8. shot[‘reload’] = function () {

  9. trace (“reload:”+ this[‘times’]);

  10. if (this[‘times’] >3) {

  11. this[‘times’] = 0;
  12. }
  13. }

  14. shot[‘reload’]();
  15. shot();
  16. shot();
  17. shot();
  18. shot();
复制代码
  1. shot();
  2. shot();

  3. /*输出:
  4. reload:0
  5. Shot():times:1
  6. reload:1
  7. Shot():times:2
  8. reload:2
  9. Shot():times:3
  10. reload:3
  11. Shot():times:4
  12. reload:4
  13. Shot():times:1
  14. reload:1
  15. Shot():times:2
  16. reload:2
  17. */
复制代码
写在篇尾的话:可以看出,运用动态添加属性和方法的技巧,可以使Function这个特殊的东西异常强大。而且其灵活程度更是空前,试想如果动态添加的方法返回函数(见4.4.1节第一个技巧)。不要忘了,动态添加的方法可直接访问函数的输入参数,那么其衍生的技巧又有多少种呢?函数又可以变成怎样一种强有力的编程对象呢?只有想不到,没有做不到。这就是Function给我们展示的无穷灵活性。
        但是,还是要说的是,技巧终归是技巧,它有其两面性。灵活是它的优点,也是它的缺点。小范围地运用让你爽快无比;大项目中大范围地使用,除非管理得很好,不然会让你头疼欲裂死而后快。
         4.5 本章小结函数是面向过程编程的核心元素,也是遥望面向对象编程殿堂的第一个界碑。其重要性不言而喻。
         本章讲解了ActionScript3中函数的两种定义法方法:语句定义法和表达式定义法,并花大篇幅详细地说明了二者的不同。并在函数本质一节中,点出了二者在底层实现上的不同。本章还介绍了ActionScript3中新增的函数特色、默认参数和…(rest)关键字。
在函数高级使用技巧中,介绍了ActionScript3函数特有的使用技巧。我们会发现随着不断挖掘函数自身的潜力,在慢慢地开始向面向对象思想靠拢。
        本章之后,我们将正式接触令人激动的面向对象编程部分。
面向对象是一种思想,是我们考虑事情的方法,通常表现为我们是将问题的解决按照过程方式来解决呢,还是将问题抽象为一个对象来解决它。很多情况下,我们会不知不觉地按照过程方式来解决它,因为我们通常习惯于考虑解决问题的过程,而不是考虑将要解决问题抽象为对象再去解决它。然而,事实上,所有复杂的问题使用面向对象来解决,往往要比使用面向过程的方法简单得多。这种美妙的体会,不是一句两句能说清的。一起来领略面向对象的巨大魅力吧,只有真正使用面向对象思维来编程,才能发挥ActionScript3全部的威力。

原创粉丝点击