函数

函数是执行特定任务并可以在程序中重用的代码块。ActionScript 3.0 中有两种函数类型:方法函数闭包。将函数称为方法还是函数闭包取决于定义函数的上下文。如果您将函数定义为类定义的一部分或者将它附加到对象的实例,则该函数称为方法。如果您以其它任何方式定义函数,则该函数称为函数闭包。

函数在 ActionScript 中始终扮演着极为重要的角色。例如,在 ActionScript 1.0 中,不存在 class 关键字,因此“类”由构造函数定义。尽管之后的 ActionScript 版本中已经添加了 class 关键字,但是,如果您想充分利用该语言所提供的功能,深入了解函数仍然十分重要。对于希望 ActionScript 函数的行为与 C++ 或 Java 等语言中的函数的行为相似的程序员来说,这可能是一个挑战。尽管基本的函数定义和调用对有经验的程序员来说应不是什么问题,但是仍需要对 ActionScript 函数的一些更高级功能进行解释。

函数的基本概念

调用函数

可通过使用后跟小括号运算符 (()) 的函数标识符来调用函数。要发送给函数的任何函数参数都括在小括号中。例如,trace() 函数在 ActionScript 3.0 中是一个顶级函数:

trace("Use trace to help debug your script");

如果要调用没有参数的函数,则必须使用一对空的小括号。例如,可以使用没有参数的 Math.random() 方法来生成一个随机数:

var randomNum:Number = Math.random();

定义您自己的函数

在 ActionScript 3.0 中可通过两种方法来定义函数:使用函数语句和使用函数表达式。您可以根据自己的编程风格(偏于静态还是偏于动态)来选择相应的方法。如果您倾向于采用静态或严格模式的编程,则应使用函数语句来定义函数。如果您有特定的需求,需要用函数表达式来定义函数,则应这样做。函数表达式更多地用在动态编程或标准模式编程中。

函数语句

函数语句是在严格模式下定义函数的首选方法。函数语句以 function 关键字开头,后跟:

  • 函数名

  • 用小括号括起来的逗号分隔参数列表

  • 用大括号括起来的函数体 — 即在调用函数时要执行的 ActionScript 代码

例如,下面的代码创建一个定义一个参数的函数,然后将字符串“hello”用作参数值来调用该函数:

function traceParameter(aParam:String) 
{ 
    trace(aParam); 
} 
 
traceParameter("hello"); // hello

函数表达式

声明函数的第二种方法就是结合使用赋值语句和函数表达式,函数表达式有时也称为函数字面值或匿名函数。这是一种较为繁杂的方法,在早期的 ActionScript 版本中广为使用。

带有函数表达式的赋值语句以 var 关键字开头,后跟:

  • 函数名

  • 冒号运算符 (:)

  • 指示数据类型的 Function

  • 赋值运算符 (=)

  • function 关键字

  • 用小括号括起来的逗号分隔参数列表

  • 用大括号括起来的函数体 — 即在调用函数时要执行的 ActionScript 代码

    例如,下面的代码使用函数表达式来声明 traceParameter 函数:

    var traceParameter:Function = function (aParam:String) 
    { 
        trace(aParam); 
    }; 
    traceParameter("hello"); // hello

    请注意,就像在函数语句中一样,在上面的代码中也没有指定函数名。函数表达式和函数语句的另一个重要区别是,函数表达式是表达式,而不是语句。这意味着函数表达式不能独立存在,而函数语句则可以。函数表达式只能用作语句(通常是赋值语句)的一部分。下面的示例显示了一个赋予数组元素的函数表达式:

    var traceArray:Array = new Array(); 
    traceArray[0] = function (aParam:String) 
    { 
        trace(aParam); 
    }; 
    traceArray[0]("hello");

在函数语句和函数表达式之间进行选择

原则上,除非在特殊情况下要求使用表达式,否则应使用函数语句。函数语句较为简洁,而且与函数表达式相比,更有助于保持严格模式和标准模式的一致性。

函数语句比包含函数表达式的赋值语句更便于阅读。与函数表达式相比,函数语句使代码更为简洁而且不容易造成混淆,因为函数表达式既需要 var 关键字又需要 function 关键字。

函数语句更有助于保持严格模式和标准模式的一致性,因为在这两种编译器模式下,均可借助点语法来调用使用函数语句声明的方法。但这对于用函数表达式声明的方法却不一定成立。例如,下面的代码定义了一个具有下面两个方法的 Example 类:methodExpression()(用函数表达式声明)和 methodStatement()(用函数语句声明)。在严格模式下,不能使用点语法来调用 methodExpression() 方法。

class Example 
{ 
var methodExpression = function() {} 
function methodStatement() {} 
} 
 
var myEx:Example = new Example(); 
myEx.methodExpression(); // error in strict mode; okay in standard mode 
myEx.methodStatement(); // okay in strict and standard modes

一般认为,函数表达式更适合于关注运行时行为或动态行为的编程。如果您喜欢使用严格模式,但是还需要调用使用函数表达式声明的方法,则可以使用这两种方法中的任一方法。首先,可以使用中括号 ([]) 代替点运算符 (.) 来调用该方法。下面的方法调用在严格模式和标准模式下都能够成功执行:

myExample["methodLiteral"]();

第二,您可以将整个类声明为动态类。尽管这样您就可以使用点运算符来调用方法,但缺点是,该类的所有实例在严格模式下都将丢失一些功能。例如,如果您尝试访问动态类实例的未定义属性,则编译器不生成错误。

在某些情况下,函数表达式非常有用。函数表达式的一个常见用法就是用于那些使用一次后便丢弃的函数。另一个用法就是向原型属性附加函数,这个用法不太常见。有关详细信息,请参阅原型对象

函数语句与函数表达式之间有两个细微的区别,在选择要使用的方法时,应考虑这两个区别。第一个区别体现在内存管理和垃圾回收方面,因为函数表达式不像对象那样独立存在。换言之,当您将某个函数表达式分配给另一个对象(如数组元素或对象属性)时,就会在代码中创建对该函数表达式的唯一引用。如果该函数表达式所附加到的数组或对象脱离作用域或由于其它原因不再可用,您将无法再访问该函数表达式。如果删除该数组或对象,该函数表达式所使用的内存就会符合垃圾回收条件,这意味着内存符合回收条件并可重新用于其它用途。

下面的示例说明对于函数表达式,一旦删除该表达式所赋予的属性,该函数就不再可用。Test 类是动态的,这意味着您可以添加一个名为 functionExp 的属性来保存函数表达式。functionExp() 函数可以用点运算符来调用,但是一旦删除了 functionExp 属性,就无法再访问该函数。

dynamic class Test {} 
var myTest:Test = new Test(); 
 
// function expression  
myTest.functionExp = function () { trace("Function expression") }; 
myTest.functionExp(); // Function expression 
delete myTest.functionExp; 
myTest.functionExp(); // error

另一方面,如果该函数最初是用函数语句定义的,那么,该函数将以对象的形式独立存在,即使在您删除它所附加到的属性之后,该函数仍将存在。delete 运算符仅适用于对象的属性,因此,即使是用于删除 stateFunc() 函数本身的调用也不工作。

dynamic class Test {} 
var myTest:Test = new Test(); 
 
// function statement 
function stateFunc() { trace("Function statement") } 
myTest.statement = stateFunc; 
myTest.statement(); // Function statement 
delete myTest.statement; 
delete stateFunc; // no effect 
stateFunc();// Function statement 
myTest.statement(); // error

函数语句与函数表达式之间的第二个区别是,函数语句存在于定义它们的整个作用域(包括出现在该函数语句前面的语句)内。与之相反,函数表达式只是为后续的语句定义的。例如,下面的代码能够在定义 scopeTest() 函数之前成功调用该函数:

statementTest(); // statementTest 
 
function statementTest():void 
{ 
    trace("statementTest"); 
}

函数表达式只有在定义之后才可用,因此,下面的代码会生成运行时错误:

expressionTest(); // run-time error 
 
var expressionTest:Function = function () 
{ 
    trace("expressionTest"); 
}

从函数中返回值

要从函数中返回值,请使用后跟要返回的表达式或字面值的 return 语句。例如,下面的代码返回一个表示参数的表达式:

function doubleNum(baseNum:int):int 
{ 
    return (baseNum * 2); 
}

请注意,return 语句会终止该函数,因此,不会执行位于 return 语句下面的任何语句,如下所示:

function doubleNum(baseNum:int):int { 
    return (baseNum * 2); 
    trace("after return"); // This trace statement will not be executed. 
}

在严格模式下,如果您选择指定返回类型,则必须返回相应类型的值。例如,下面的代码在严格模式下会生成错误,因为它们不返回有效值:

function doubleNum(baseNum:int):int 
{ 
    trace("after return"); 
}

嵌套函数

您可以嵌套函数,这意味着函数可以在其它函数内部声明。除非将对嵌套函数的引用传递给外部代码,否则嵌套函数将仅在其父函数内可用。例如,下面的代码在 getNameAndVersion() 函数内部声明两个嵌套函数:

function getNameAndVersion():String 
{ 
    function getVersion():String 
    { 
        return "10"; 
    } 
    function getProductName():String 
    { 
        return "Flash Player"; 
    } 
    return (getProductName() + " " + getVersion()); 
} 
trace(getNameAndVersion()); // Flash Player 10

在将嵌套函数传递给外部代码时,它们将作为函数闭包传递,这意味着嵌套函数保留在定义该函数时处于作用域内的任何定义。有关详细信息,请参阅函数作用域

函数参数

ActionScript 3.0 为函数参数提供了一些功能,这些功能对于那些刚接触 ActionScript 语言的程序员来说可能是很陌生的。尽管大多数程序员都熟悉按值或按引用传递参数这一概念,但是很多人可能对 arguments 对象和 ... (rest) 参数感到很陌生。

按值或按引用传递参数

在许多编程语言中,一定要了解按值传递参数与按引用传递参数之间的区别;这种区别会影响代码的设计方式。

按值传递意味着将参数的值复制到局部变量中以便在函数内使用。按引用传递意味着将只传递对参数的引用,而不传递实际值。这种方式的传递不会创建实际参数的任何副本,而是会创建一个对变量的引用并将它作为参数传递,并且会将它赋给局部变量以便在函数内部使用。局部变量是对函数外部的变量的引用,它使您能够更改初始变量的值。

在 ActionScript 3.0 中,所有的参数均按引用传递,因为所有的值都存储为对象。但是,属于基元数据类型(包括 Boolean、Number、int、uint 和 String)的对象具有一些特殊运算符,这使它们可以像按值传递一样工作。例如,下面的代码创建一个名为 passPrimitives() 的函数,该函数定义了两个类型均为 int、名称分别为 xParamyParam 的参数。这些参数与在 passPrimitives() 函数体内声明的局部变量类似。当使用 xValueyValue 参数调用函数时,xParamyParam 参数将用对 int 对象(由 xValueyValue 表示)的引用进行初始化。因为参数是基元值,所以它们像按值传递一样工作。尽管 xParamyParam 最初仅包含对 xValueyValue 对象的引用,但是,对函数体内的变量的任何更改都会导致在内存中生成这些值的新副本。

function passPrimitives(xParam:int, yParam:int):void 
{ 
    xParam++; 
    yParam++; 
    trace(xParam, yParam); 
} 
 
var xValue:int = 10; 
var yValue:int = 15; 
trace(xValue, yValue);// 10 15 
passPrimitives(xValue, yValue); // 11 16 
trace(xValue, yValue);// 10 15

passPrimitives() 函数内部,xParamyParam 的值递增,但这不会影响 xValueyValue 的值,如上一条 trace 语句所示。即使参数的命名与 xValueyValue 变量的命名完全相同也是如此,因为函数内部的 xValueyValue 将指向内存中的新位置,这些位置不同于函数外部同名的变量所在的位置。

其它所有对象(即不属于基元数据类型的对象)始终按引用传递,这样您就可以更改初始变量的值。例如,下面的代码创建一个名为 objVar 的对象,该对象具有两个属性:xy。该对象作为参数传递给 passByRef() 函数。因为该对象不是基元类型,所以它不但按引用传递,而且还保持一个引用。这意味着对函数内部的参数的更改会影响到函数外部的对象属性。

function passByRef(objParam:Object):void 
{ 
    objParam.x++; 
    objParam.y++; 
    trace(objParam.x, objParam.y); 
} 
var objVar:Object = {x:10, y:15}; 
trace(objVar.x, objVar.y); // 10 15 
passByRef(objVar); // 11 16 
trace(objVar.x, objVar.y); // 11 16

objParam 参数与全局 objVar 变量引用相同的对象。正如在本示例的 trace 语句中所看到的一样,对 objParam 对象的 xy 属性所做的更改将反映在 objVar 对象中。

默认参数值

在 ActionScript 3.0 中,可以为函数声明默认参数值。如果在调用具有默认参数值的函数时省略了具有默认值的参数,那么,将使用在函数定义中为该参数指定的值。所有具有默认值的参数都必须放在参数列表的末尾。指定为默认值的值必须是编译时常量。如果某个参数存在默认值,其效果会使该参数成为可选参数。没有默认值的参数被视为必需参数

例如,下面的代码创建一个具有三个参数的函数,其中的两个参数具有默认值。当仅用一个参数调用该函数时,将使用这些参数的默认值。

function defaultValues(x:int, y:int = 3, z:int = 5):void 
{ 
    trace(x, y, z); 
} 
defaultValues(1); // 1 3 5

arguments 对象

在将参数传递给某个函数时,可以使用 arguments 对象来访问有关传递给该函数的参数的信息。arguments 对象的一些重要方面包括:

  • arguments 对象是一个数组,其中包括传递给函数的所有参数。

  • arguments.length 属性报告传递给函数的参数数量。

  • arguments.callee 属性提供对函数本身的引用,这对于递归调用函数表达式很有用。

    注: 如果将任何参数命名为 arguments,或者使用 ...(rest) 参数,则 arguments 对象不可用。

    如果在函数体中引用了 arguments 对象,则在 ActionScript 3.0 中,函数调用中所包括的参数的数量可以大于在函数定义中所定义的参数数量,但是,如果参数的数量与必需参数(以及可选情况下的任何可选参数)的数量不匹配,则在严格模式下会生成编译器错误。您可以使用 arguments 对象的数组样式来访问传递给函数的任何参数,而无需考虑是否在函数定义中定义了该参数。下面的示例(仅在标准模式下进行编译)使用 arguments 数组及 arguments.length 属性来跟踪传递给 traceArgArray() 函数的所有参数:

    function traceArgArray(x:int):void 
    { 
        for (var i:uint = 0; i < arguments.length; i++) 
        { 
            trace(arguments[i]); 
        } 
    } 
     
    traceArgArray(1, 2, 3); 
     
    // output: 
    // 1 
    // 2 
    // 3

    arguments.callee 属性通常用在匿名函数中以创建递归。您可以使用它来提高代码的灵活性。如果递归函数的名称在开发周期内的不同阶段会发生改变,而且您使用的是 arguments.callee(而非函数名),则不必花费精力在函数体内更改递归调用。下面的函数表达式中使用 arguments.callee 属性来支持递归:

    var factorial:Function = function (x:uint) 
    { 
        if(x == 0) 
        { 
            return 1; 
        } 
        else 
        { 
            return (x * arguments.callee(x - 1)); 
        } 
    } 
     
    trace(factorial(5)); // 120

    如果在函数声明中使用 ... (rest) 参数,则不能使用 arguments 对象,而必须使用为参数声明的参数名来访问参数。

    还应避免将“arguments”字符串用作参数名,因为该字符串会遮蔽 arguments 对象。例如,如果重写 traceArgArray() 函数,以便添加 arguments 参数,那么,函数体内对 arguments 的引用所引用的将是该参数,而不是 arguments 对象。下面的代码不生成输出结果:

    function traceArgArray(x:int, arguments:int):void 
    { 
        for (var i:uint = 0; i < arguments.length; i++) 
        { 
            trace(arguments[i]); 
        } 
    } 
     
    traceArgArray(1, 2, 3); 
     
    // no output

    在早期的 ActionScript 版本中,arguments 对象还包含一个名为 caller 的属性,该属性是对调用了当前函数的函数的引用。ActionScript 3.0 中没有 caller 属性,但是,如果您需要引用调用函数,则可以更改调用函数,以使其传递一个额外的参数来引用其本身。

... (rest) 参数

ActionScript 3.0 中引入了一个称为 ... (rest) 参数的新参数声明。此参数可用来指定一个数组参数以接受任意多个以逗号分隔的参数。参数可以拥有保留字以外的任意名称。此参数声明必须是最后一个指定的参数。使用此参数会使 arguments 对象不可用。尽管 ... (rest) 参数提供了与 arguments 数组和 arguments.length 属性相同的功能,但是未提供与由 arguments.callee 提供的功能类似的功能。使用 ...(rest) 参数之前,应确保不需要使用 arguments.callee

下面的示例使用 ... (rest) 参数(而不是 arguments 对象)来重写 traceArgArray() 函数:

function traceArgArray(... args):void 
{ 
    for (var i:uint = 0; i < args.length; i++) 
    { 
        trace(args[i]); 
    } 
} 
 
traceArgArray(1, 2, 3); 
 
// output: 
// 1 
// 2 
// 3

... (rest) 参数还可与其它参数一起使用,前提是 ... (rest) 参数是列出的最后一个参数。下面的示例修改 traceArgArray() 函数,以便其第一个参数 x 是 int 类型,第二个参数使用 ... (rest) 参数。输出结果会忽略第一个值,因为第一个参数不再属于由 ...(rest) 参数创建的数组。

function traceArgArray(x: int, ... args) 
{ 
    for (var i:uint = 0; i < args.length; i++) 
    { 
        trace(args[i]); 
    } 
} 
 
traceArgArray(1, 2, 3); 
 
// output: 
// 2 
// 3

函数作为对象

ActionScript 3.0 中的函数是对象。当您创建函数时,就是在创建对象,该对象不仅可以作为参数传递给另一个函数,而且还可以有附加的属性和方法。

作为参数传递给另一个函数的函数是按引用(而不是按值)传递的。在将某个函数作为参数传递时,只能使用标识符,而不能使用在调用方法时所用的小括号运算符。例如,下面的代码将名为 clickListener() 的函数作为参数传递给 addEventListener() 方法:

addEventListener(MouseEvent.CLICK, clickListener);

尽管刚接触 ActionScript 的程序员可能对此感觉有些奇怪,但是,函数确实可以像其它任何对象那样具有属性和方法。实际上,每个函数都有一个名为 length 的只读属性,它用来存储为该函数定义的参数数量。该属性与 arguments.length 属性不同,后者报告发送给函数的参数数量。回想一下,在 ActionScript 中,发送给函数的参数数量可以超过为该函数定义的参数数量。下面的示例说明了这两个属性之间的区别,此示例仅在标准模式下编译,因为严格模式要求所传递的参数数量与所定义的参数数量完全相同:

// Compiles only in standard mode 
function traceLength(x:uint, y:uint):void 
{ 
    trace("arguments received: " + arguments.length); 
    trace("arguments expected: " + traceLength.length); 
} 
 
traceLength(3, 5, 7, 11); 
/* output: 
arguments received: 4 
arguments expected: 2 */

在标准模式下,可以通过在函数体外部定义函数属性来定义您自己的函数属性。函数属性可以用作准静态属性,用来保存与该函数有关的变量的状态。例如,您可能希望跟踪对特定函数的调用次数。如果您正在编写游戏,并且希望跟踪用户使用特定命令的次数,则这样的功能会非常有用,尽管您也可以使用静态类属性来实现此目的。下面的示例(该示例仅在标准模式下进行编译,因为严格模式不允许向函数添加动态属性)在函数声明外部创建一个函数属性,并在每次调用该函数时递增此属性:

// Compiles only in standard mode 
var someFunction:Function = function ():void 
{ 
    someFunction.counter++; 
} 
 
someFunction.counter = 0; 
 
someFunction(); 
someFunction(); 
trace(someFunction.counter); // 2

函数作用域

函数的作用域不但决定了可以在程序中的什么位置调用函数,而且还决定了函数可以访问哪些定义。适用于变量标识符的作用域规则同样也适用于函数标识符。在全局作用域中声明的函数在整个代码中都可用。例如,ActionScript 3.0 包含可在代码中的任意位置使用的全局函数,如 isNaN()parseInt()。嵌套函数(即在另一个函数中声明的函数)可以在声明它的函数中的任意位置上使用。

作用域链

无论何时开始执行函数,都会创建许多对象和属性。首先,会创建一个称为激活对象的特殊对象,该对象用于存储在函数体内声明的参数以及任何局部变量或函数。由于激活对象属于内部机制,因此您无法直接访问它。接着,会创建一个作用域链,其中包含由运行时检查标识符声明的对象的有序列表。所执行的每个函数都有一个存储在内部属性中的作用域链。对于嵌套函数,作用域链始于其自己的激活对象,后跟其父函数的激活对象。作用域链以这种方式延伸,直到到达全局对象。全局对象是在 ActionScript 程序开始时创建的,其中包含所有的全局变量和函数。

函数闭包

函数闭包是一个对象,其中包含函数的快照及其词汇环境。函数的词汇环境包括函数作用域链中的所有变量、属性、方法和对象以及它们的值。无论何时在对象或类之外的位置执行函数,都会创建函数闭包。函数闭包保留定义它们的作用域,这样,在将函数作为参数或返回值传递给另一个作用域时,会产生有趣的结果。

例如,下面的代码创建两个函数:foo()(返回一个用来计算矩形面积的嵌套函数 rectArea())和 bar()(调用 foo() 并将返回的函数闭包存储在名为 myProduct 的变量中)。即使 bar() 函数定义了自己的局部变量 x(值为 2),当调用函数闭包 myProduct() 时,该函数闭包仍保留在函数 foo() 中定义的变量 x(值为 40)。因此,bar() 函数会返回值 160,而不是 8

function foo():Function 
{ 
    var x:int = 40; 
    function rectArea(y:int):int // function closure defined 
    { 
        return x * y 
    }  
    return rectArea; 
} 
function bar():void 
{ 
    var x:int = 2; 
    var y:int = 4; 
    var myProduct:Function = foo(); 
    trace(myProduct(4)); // function closure called 
} 
bar(); // 160

方法的行为与函数闭包类似,因为方法也保留有关创建它们的词汇环境的信息。当方法提取自它的实例(这会创建绑定方法)时,此特征尤为突出。函数闭包与绑定方法之间的主要区别在于,绑定方法中 this 关键字的值始终引用它最初附加到的实例,而函数闭包中 this 关键字的值可以改变。