第4章 函数
函数在程序设计的过程中是一个“革命性”的创新。利用函数编程,可以避免冗长、杂乱的代码;利用函数编程,可以重复利用代码,提高程序效率;利用函数编程,可以便利地修改程序,提高编程效率。
函数(function)的准确定义为:执行特定任务,并可以在程序中重用的代码块。ActionScript 3.0中有两类函数:“方法”和“函数闭包”。具体是将函数称为方法还是函数闭包,取决于定义函数的上下文。
4.1 定义函数
在ActionScript 3.0中有两种定义函数的方法:一种是常用的函数语句定义法,一种是ActionScript中独有的函数表达式定义法。具体使用哪一种方法来定义,要根据编程习惯来选择。一般的编程人员使用函数语句定义法,对于有特殊需求的编程人员,则使用函数表达式定义法。
4.1.1 函数语句定义法
函数语句定义法是程序语言中基本类似的定义方法,使用function关键字来定义,其格式如下所示:
function 函数名(参数1:参数类型,参数2:参数类型...):返回类型{ //函数体 }
代码格式说明:
❑function:定义函数使用的关键字,function关键字要以小写字母开头。
❑函数名:定义函数的名称。函数名要符合变量命名的规则,最好给函数取一个与其功能一致的名字。
❑小括号:定义函数的必需格式,小括号内的参数和参数类型均为可选。
❑返回类型:定义函数的返回类型,也是可选的。要设置返回类型,冒号和返回类型必须成对出现,而且返回类型必须是存在的类型。
❑大括号:定义函数的必需格式,需要成对出现。括起来的是函数定义的程序内容,是调用函数时执行的代码。
下面的代码定义一个求和sum函数,并调用函数,输出调用结果。代码如下所示:
function sum(a:int,b:int):int { //return为返回关键字 return a + b; } trace(sum(1,2));//输出:3
4.1.2 函数表达式定义法
函数表达式定义法有时也称为函数字面值或匿名函数。这是一种较为繁杂的方法,在早期的ActionScript版本中广为使用。其格式如下所示:
var 函数名:Function=function(参数1:参数类型,参数2:参数类型...):返回类型{ //函数体 }
代码格式说明:
❑var:定义函数名的关键字,var关键字要以小写字母开头。
❑函数名:定义的函数名称。
❑Function:指示定义数据类型是Function类。注意Function为数据类型,需大写字母开头。
❑=:赋值运算符,把匿名函数赋值给定义的函数名。
❑function:定义函数的关键字,指明定义的是函数。
❑小括号:定义函数的必需格式,小括号内的参数和参数类型均为可选。
❑返回类型:定义函数的返回类型,可选参数。
❑大括号:其中为函数要执行的代码。
需要注意的是,该方法首先定义的是一个匿名函数,然后利用赋值运算符“=”把该函数赋值给定义的函数名。
下面是一个使用函数表达式定义法定义函数的示例,其代码如下所示:
var sum:Function=function(a:int,b:int):int { return a + b; }
4.1.3 两种定义方法的区别和选择
原则上推荐使用函数语句定义法,因为这种方法更加简洁,更有助于保持严格模式和标准模式的一致性。
下面要讲解的是函数语句和函数表达式定义法之间存在的区别,具体内容如下所示。
1. 两种方法的区别
函数语句和函数表达式定义法的作用域不同。函数语句作用于定义它的整个作用域(包括函数前面的语句)内,而函数表达式定义法的作用域只存在于其定义之后。简单地说,函数语句定义法定义的函数,无论在函数语句之前调用函数还是之后调用函数,函数都可以被调用;而函数表达式定义法则必须先定义后调用,否则编译就会报错。
下面使用函数语句法定义一个函数hello(),并在定义之前调用,其代码如下:
hello(); function hello() { var str:String="函数语句法定义前后都可以调用"; trace(str);//输出:函数语句法定义前后都可以调用 }
下面使用函数语句法定义一个函数hello(),并在定义之后调用,其代码如下:
function hello() {
var str:String="函数语句法定义前后都可以调用";
trace(str);
}
hello();
下面使用函数表达式法定义一个函数hello(),并在定义之前调用,其代码如下:
hello();
var hello:Function=function(){
var str:String="函数表达式定义法不能提前调用";
trace(str);
};
注意 此段代码不能正确编译,输出提示为“value不是函数”,说明此时函数hello()还没有被定义。
下面使用函数表达式法定义一个函数hello(),并在定义之后调用,其代码如下:
var hello:Function=function(){
var str:String="函数表达式定义法定义之后才能调用";
trace(str);
};
hello();
函数语句和函数表达式定义法对this关键字的指向不同。函数语句定义法,this关键字永远指向当前函数定义的域;而表达式定义法由于是匿名函数定义后被赋值为定义的函数变量,所以this的指向会随着依附对象的不同而不同。
函数语句和表达式定义法在内存管理和垃圾回收方面也存在不同。因为函数表达式不像对象那样独立存在,它是一个匿名函数。当引用这个函数的对象由于其他原因不再可用,那么将无法访问该函数。
函数语句比包含函数表达式的赋值语句更便于阅读。对比两种定义方法,可以发现,表达式定义法要求的语法内容更多,代码更加复杂,更容易造成编程时的混乱。
2. 两种方法的选择
在两种定义方法的选择上,一般使用函数语句定义法。函数表达式定义函数主要用于:一是适合关注运行时行为或动态行为的编程;二是用于那些使用一次后便丢弃的函数或者向原型属性附加的函数。函数表达式更多地用在动态编程或标准模式编程中。
4.2 调用函数
函数只是一个编好的程序块,在没有被调用之前,什么也不会发生。只有通过调用函数,函数的功能才能够实现,才能体现出函数的高效率。通过本节的学习,读者将掌握一般的函数调用方法以及嵌套和递归调用函数的方法。
4.2.1 函数的一般调用
对于没有参数的函数,可以直接使用该函数的名字,并后跟一个圆括号(它被称为“函数调用运算符”)来调用。
下面定义一个不带参数的函数HelloAS(),并在定义之后直接调用,其代码如下:
function HelloAS() {
trace("AS 3.0世界欢迎你!");
}
HelloAS();
代码运行后的输出结果如下所示。
//输出:AS 3.0世界欢迎你!
如果函数本身没有参数,就不能在调用的括号中输入参数,否则就会报错。
下面定义一个不带参数的函数HelloAS(),调用时输入参数“我要走进AS 3.0世界”,其代码如下:
function HelloAS() { trace("欢迎走进AS 3.0世界"); } HelloAS("我要走进AS 3.0世界");
注意 此段代码不能正确编译,输出提示为“参数个数不正确,应该为0个”,说明该函数无参数,而调用输入了参数。
对于含有参数的函数,如果参数本身设有默认值,则函数名后的括号内可以输入参数,也可以不输入参数。
下面定义一个含有默认参数的函数HelloAS(),调用时不输入参数,其代码如下:
function HelloAS(str:String="AS 3.0世界欢迎你!") {
trace(str);
}
HelloAS();
代码运行后的输出结果如下所示:
//输出:AS 3.0世界欢迎你!
但是,如果参数没有设置默认值,那么函数名后的括号内必须输入参数,而且输入的参数类型必须和默认的参数类型一致,否则就会报错。
下面定义一个含有默认参数的函数HelloAS(),调用时输入参数“我要走进AS 3.0世界”,其代码如下:
function HelloAS(str:String="AS 3.0世界欢迎你!") { trace(str); } HelloAS("我要走进AS 3.0世界");
代码运行后的输出结果如下所示:
//输出:我要走进AS 3.0世界
下面定义一个含有参数的函数HelloAS(),调用时未输入参数,其代码如下:
function HelloAS(str:String) { trace(str); } HelloAS();
注意 此段代码不能正确编译,输出提示为“参数个数不正确,应该为1个”,说明该函数有一个参数,而调用没有输入参数。
下面定义一个含有参数的函数HelloAS(),调用时输入参数的数据类型和默认参数的数据类型不同,其代码如下:
function HelloAS(str:String) { trace(str); } HelloAS(1);
注意 此段代码不能正确编译,输出提示为“int类型值的隐式强制指令的目标是非相关类型String”,说明输入参数的数据类型是int,而不是默认的数据类型String。
当然,如果函数的参数没有指定类型,则输入的参数类型没有强制要求。
另外,还可以通过函数的引用,把一个函数赋值给另一个变量。一旦将对一个函数的引用赋给一个变量,就可以将那个变量名与函数调用运算符结合起来调用那个函数。
下面的代码把一个定义好的函数“求和”赋值给变量“sum”,并利用“sum”变量来调用函数“求和”:
function 求和(a:int,b:int) { trace(a+b); } var sum:Function=求和; sum(1,2);
代码运行后的输出结果如下所示:
3
4.2.2 嵌套和递归调用函数
嵌套和递归调用是两种类似的函数调用方式,其本质都是在调用函数时用一个函数调用另一个函数。不同的是,嵌套调用是用一个函数去调用另一个函数,而递归调用是函数调用自身函数。
1. 嵌套调用函数
AS 3.0允许函数嵌套定义,即在一个函数体中再定义一个新函数。嵌套的层次可以是多层的,比如函数A可以调用函数B,而函数B又可以调用函数C⋯⋯
下面先定义求整数和sum()函数,它接受两个参数:一个字符型参数a和一个数字型参数b。为了求和,需要把参数a转换为整数类型,把参数b转换为整数类型。为此在函数体中定义STI()函数把字符型转为整数型,定义NTR()函数把数字进行求整。程序代码如下:
function sum(a:String,b:Number) { function STI(c:String):int { return int(c); } function NTR(d:Number):int { return Math.round(d); } return STI(a) + NTR(b); } var a:String="10"; var b:Number=5.56; trace(sum(a,b));
代码运行后的输出结果如下所示:
16
注意 在嵌套函数内部定义的函数,仅在主函数体内可用,除非将对嵌套函数的引用传递给外部代码。
此外,嵌套函数也可以调用和自身函数并列的函数,即允许在一个函数体内调用另一个函数。同样的功能,使用下面的代码也可以实现。
function sum(a:String,b:Number) { return STI(a) + NTR(b); } var a:String="10"; var b:Number=5.56; trace(sum(a,b)); function STI(c:String):int { return int(c); } function NTR(d:Number):int { return Math.round(d); }
代码运行后的输出结果如下所示:
16
注意 使用此种方法嵌套调用函数,外部的函数对整个作用域都是可用的。
2. 递归调用函数
程序调用自身的编程技巧称为递归。递归调用是一个函数在其定义或说明中直接或间接调用自身的方法,它能够把复杂的、大型的程序转化成和原问题相似的小问题来处理。
下面的代码使用递归函数的方法来求1+2+3+⋯+n的值,其代码如下:
function sum(n:int):int { if (n==1) { return 1; } else { return n + sum(n - 1); } } trace(sum(100));
代码运行后的输出结果如下所示:
5050
注意 递归必须是有条件的,在定义的时候必须有一个明确的递归结束条件,称为递归出口。缺少递归出口的代码将永远地调用自身,形成死循环。
如下面的递归求阶乘(n!)的代码是不正确的。
function fac(n:int):int { return n * fac(n - 1); } trace(fac(100));
但是,Flash中对此有一个保护措施,在一定的递归次数之后,系统会终止ActionScript代码的运行,并给出提示信息。
4.3 函数的返回值
主调函数通过函数的调用得到一个确定的值,此值被称为函数的返回值。利用函数的返回值,可以通过函数进行数据的处理、分析和转换,并能最终获取想要获得的结果。在本节我们主要学习函数返回值的获取方法和获取过程中的注意事项。
4.3.1 return语句
AS从函数中获取返回值,使用return语句来实现,语法格式如下:
return 返回值
格式说明如下:
❑return:函数返回值的关键字,必需的。
❑返回值:函数中返回的数据,既可以是字符串、数值等,也可以是对象,如数组、影片简介等。
下面定义一个求圆形面积的函数,并返回圆面积的值,其代码如下:
function 圆面积(r:Number):Number{ var s:Number=Math.PI*r*r; return s; } trace(圆面积(5));
代码运行后的输出结果如下所示:
78.53981633974483
上面的代码中,return后的返回值是一个变量s,return后的返回值也可以是一个表达式。把上面的代码修改如下,也可以实现相同的效果。
function 圆面积(r:Number):Number{ return Math.PI*r*r; } trace(圆面积(5));
代码运行后的输出结果如下所示:
78.53981633974483
return语句在使用时还要注意下面几点:
❑同一个函数中可以有多个return语句,执行到哪个return语句,哪一个return语句起作用。
下面的代码在输入不同的参数时,执行的返回语句不同。
function 选择输出(x:Number):Number { if (x>0) { return 1; } else if (x<0) { return -1; } else { return 0; } } trace(选择输出(10));
代码运行后的输出结果如下所示:
1
说明
在输入的参数大于0时,执行return 1语句;参数小于0,执行return-1语句;参数等于0,执行return 0语句。
❑return语句同时又是一个终止语句,只要执行到return语句,Flash就终止执行之后的代码,跳出函数体。
从下面代码的输出结果,可发现return前的输出语句可以执行,而return后的输出语句没有被执行。
function 输出测试() { trace("这是return前的语句,能够被执行"); return; trace("这是return后的语句,不能被执行"); } 输出测试();
代码运行后的输出结果如下所示:
这是return前的语句,能够被执行
❑return语句后的返回值可以为空,此时返回为“undefined”。
下面的return语句没有参数,其代码如下:
function 输出测试() { return; } trace(输出测试());
代码运行后的输出结果如下所示:
undefined
4.3.2 返回值类型
函数的返回类型在函数的定义中属于可选参数,如果没有选择,那么返回值的类型由return语句中返回值的数据类型来决定。
下面的代码,return语句返回一个字符型数据,来验证一下返回值的类型。
function 类型测试() { var a:String="这是一个字符串"; return a; } trace(typeof(类型测试()));
代码运行后的输出结果如下所示:
string
如果函数定义过程中设定了返回类型,则返回值的类型必须和设置的数据类型相同,否则编译就会报错。
把上面的函数增加一个返回类型Number,代码如下:
function 类型测试():Number { var a:String="这是一个字符串"; return a; } trace(类型测试());
注意
此段代码不能正确编译,输出提示为“String类型值的隐式强制指令的目标是非相关类型Number”,说明return返回的数据类型是String,而不是默认的返回类型Number。把返回类型Number改为String,代码就能编译通过。
若函数不需要返回数据,也就是函数体中不存在return语句,那么定义函数时不能设置返回类型,否则编译会报错。下面的代码不能够被编译:
function 返回测试():String { var a:String="这是一个字符串"; trace(a); } trace(返回测试());
注意 此段代码不能正确编译,输出提示为“函数没有返回值”,说明函数体中没有返回语句。
4.4 函数的参数
函数通过参数向函数体传递数据和信息。ActionScript 3.0对函数的参数增加了一些新功能,同时也增加了一些限制。有大多数程序员都熟悉的按值或按引用传递参数这一概念,也有很多人相对陌生的arguments对象和...(rest)参数。
4.4.1 传递参数的语法
函数中传递的参数都位于函数格式的括号中,语法格式如下:
(参数1:参数类型=默认值,参数2:参数类型=默认值)
下面定义一个个性化的欢迎语句,对不同的姓名给出对应的问候。代码如下所示:
function Welcome(username:String):void { trace("欢迎你!"+username); } Welcome("张三"); Welcome("小明");
代码运行后的输出结果如下所示:
欢迎你!张三 欢迎你!小明
ActionScript函数支持传递多个参数。在定义函数时,使用半角逗号分隔开不同的参数即可。下面定义一个多个参数的函数,代码如下所示:
function intro(username:String,age:int):void { trace("你的姓名:"+username+" 年龄是:"+age); } intro("小王",25);
代码运行后的输出结果如下所示:
你的姓名:小王 年龄是:25
注意 隔开参数的是半角逗号而不是分号。
在设置参数时不能使用var语法,否则编译时就会报错。下面的代码使用var定义了参数,不能编译通过。
function Hello(var username:String):void{ trace("Hello."+username) }
4.4.2 传递参数的两种方法
许多编程语言中,参数的传递基本都是两种类型:按值或者按引用传递。按值传递意味着将参数的值复制到局部变量中以便在函数内使用。按引用传递意味着将只传递对参数的引用,而不传递实际值。要了解任何一门编程语言中的函数,首先必须搞清楚参数的传递到底是按值还是按引用。
在ActionScript 3.0中,所有的参数均按引用传递,因为所有的值都存储为对象。基元型数据是不变的对象,按值还是按引用的效果一样,通常可以看做是按值传递。按值传递,是指参数被传递给函数后,被传递的变量就独立了。若在函数中改变这个变量,原变量不会发生任何变化。
下面来测试一下,代码如下所示:
function test(a:Number):Number { a++; return a; } var b:Number=5; trace("b引用前:"+b); var c=test(b); trace("b引用后:"+b); trace("c:"+c);
代码运行后的输出结果如下所示:
b引用前:5 b引用后:5 c:6
说明
从输出的结果可以看出,变量b在引用前和引用后,其值没有变化,变化的是变量b的一个引用。
复杂类型数据不但按引用传递,而且还保持一个引用。这样,在函数内部参数的更改同样会影响到函数外部数据。
下面先建立一个复杂型数据Array对象,然后在数组中给其追加一个数据,结果发现数组的数据发生了改变。代码如下所示:
function TestArr(_arr:Array):void { var a:int=100; _arr.push(a); } var b:Array=[1,2,3]; trace("引用前:"+b); TestArr(b); trace("引用后:"+b);
代码运行后的输出结果如下所示:
引用前:1,2,3 引用后:1,2,3,100
说明
从输出的结果可以看出,数组变量b的值在引用前后发生了变化。
4.4.3 给函数设置默认参数
在ActionScript 2.0中并不支持对函数设置默认参数,此为ActionScript 3.0的新功能。要给一个函数的参数设置默认值,语法格式如下:
function(参数1:参数类型=默认值,参数2:参数类型=默认值)
默认参数是可选项,可以设置默认参数,也可以不设置默认参数。若设置了默认参数,则在调用函数时,如果没有写明参数,系统将使用在函数定义中为该参数指定的值。
下面定义一个有3个参数的函数,其中两个参数有默认值。代码如下所示:
function Test(a:int,b:int=2,c:int=3):void { trace(a,b,c); } Test(1); //输出:1,2,3 Test(1,4); //输出:1,4,3 Test(1,4,0); //输出:1,4,0
参数设置了默认值,那么在调用时即为可选参数,可以设置,也可以不设置,而没有默认值的参数则必须输入。
和上面相同的一个函数,若在调用时不设置参数,则编译时会报错。代码如下所示:
function Test(a:int,b:int=2,c:int=3):void { trace(a,b,c); } Test();
注意 此段代码不能正确编译,输出提示为“参数个数不正确,应该为1个”,说明默认参数在系统分析的时候并不计入必选参数之列。
4.4.4 arguments 对象和...(rest) 参数
ActionScript 3.0中有两种函数调用时检查参数数量的方法,分别为使用arguments对象和...(rest) 参数。
1. arguments对象
在函数中,可以使用arguments对象访问有关传递给该函数的参数信息。arguments对象是一个数组,其中按顺序保存着传递给函数的所有参数。可以使用数组的访问方式来访问传入的参数。它有一个length属性记录当前传入的参数数目,还有一个属性callee提供对函数本身的引用,该引用可用于递归调用函数表达式。
在ActionScript 3.0中,函数调用中所包括的参数的数量可以大于在函数定义中所指定的参数数量,但是,如果参数的数量小于必需参数的数量,编译时就会报错。可以通过arguments对象的数组来访问传递给函数的任何参数,不管是否在函数定义中定义了该参数。下面使用arguments数组及arguments.length属性来输出传递给TestArg()函数的所有参数。代码如下所示:
function TestArg(a:int,b:int,c:int):void { trace("输入的参数个数是:"+arguments.length); for (var i:uint = 0; i < arguments.length; i++) { trace("这是第"+i+"个参数,其值为:"+arguments[i]); } } TestArg(1,2,3);
代码运行后的输出结果如下所示:
输入的参数个数是:3 这是第0个参数,其值为:1 这是第1个参数,其值为:2 这是第2个参数,其值为:3
注意 在ActionScript 3.0中,要遵守函数的严格定义。所以ActionScript 2.0中无视函数定义,传入任意多个参数的做法在ActionScript 3.0中是非法的。
arguments.callee属性通常用在匿名函数中以创建递归,以此来提高程序的灵活性。下面的函数表达式中,使用arguments.callee属性来启用递归。代码如下所示:
var fac:Function = function (i:uint){
if(i == 1) {
return 1;
} else{
return (i + arguments.callee(i - 1));
}
};
trace(fac(100));// 输出:5050
2. ...(rest)参数
...(rest)参数是ActionScript 3.0引入的新参数声明。使用该参数可指定一个自己命名的数组参数来接受任意多个以逗号分隔的参数。其语法格式如下:
function(..args) function(参数1,参数2,...args)
...(rest)参数拥有arguments对象的储存功能和length属性,但是不再具有callee属性。
下面用...(rest)参数来重写TestArg()函数,代码如下所示:
function TestArg(...args):void { trace("输入的参数个数是:"+args.length); for (var i:uint = 0; i < args.length; i++) { trace("这是第"+i+"个参数,其值为:"+args[i]); } } TestArg(1,2,3);
代码运行后的输出结果如下所示:
输入的参数个数是:3 这是第0个参数,其值为:1 这是第1个参数,其值为:2 这是第2个参数,其值为:3
...(rest)参数还可与其他参数一起使用,但是要注意其只能是最后一个列出的参数。下面修改TestArg()函数,增加一个int型参数x,第二个参数使用...(rest)参数,输出的结果将忽略第一个值。代码如下所示:
function TestArg(x:int,...args):void { trace("...(rest) 参数个数是:"+args.length); for (var i:uint = 0; i < args.length; i++) { trace("这是第"+i+"个...(rest)参数,其值为:"+args[i]); } } TestArg(1,2,3);
代码运行后的输出结果如下所示:
...(rest) 参数个数是:2 这是第0个...(rest)参数,其值为:2 这是第1个...(rest)参数,其值为:3
注意 此时...(rest) 参数中记录的参数个数是总的参数个数减去定义过的参数。
arguments对象和...(rest)参数都可以作为一个数组储存输入的参数,但是在使用...(rest)参数时, arguments对象不可用。
4.5 练习题
1. 简述传递参数时,值类型和引用类型的区别,以及字符串String和数组Array属于哪种类型。
2. 请尝试完成:把数字1~100随机不重复放到一个数组里。
3. “String类型值的隐式强制指令的目标是非相关类型Number”,可能产生这个编译错误的原因是什么?
4. 关于函数的返回值,说法正确的是( )。
A. AS 3.0必须声明函数的返回类型
B. 可以不声明返回类型,也可以用“:void”定义函数无返回值,定义为void时,可以return null
C. 可以将函数返回类型设置为“*”号,代表函数可以返回任意类型的数据
D. 函数test():Object不能返回字符串数据
5. 在函数中,可以使用arguments对象来访问有关传递给该函数的参数的信息,arguments有个属性callee,它的用途是什么?