函數

「函數」是可以執行特定工作,且可在程式中重複使用的程式碼區塊。ActionScript 3.0 中有兩種類型的函數:「方法」和「函數結束項」。依函數定義的內容,便可決定函數該稱為方法或函數結束項。若函數是定義為類別定義的一部分,或附加至物件的實體,則稱為方法;若函數是以任何其它方式定義,則稱為函數結束項。

函數在 ActionScript 中一直都相當重要。例如,ActionScript 1.0 中沒有 class 關鍵字,所以「類別」就由建構函數加以定義。雖然已將 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");

選擇陳述式或運算式

一般原則是,除非有特定情況需要使用運算式,否則就使用函數陳述式。函數陳述式不那麼詳細而冗長,而且在嚴謹模式與標準模式之間可提供比函數運算式更為一致的體驗。

函數陳述式比包含函數運算式的指定陳述式更容易閱讀。函數陳述式可讓您的程式碼更簡潔,也沒有函數運算式那麼複雜,因為函數運算式需要同時使用 varfunction 兩個關鍵字。

函數陳述式之所以能在兩個編譯器模式之間提供更一致的體驗,在於您可以同時在嚴謹模式與標準模式使用點語法,叫用使用函數陳述式宣告的方法。而用函數運算式宣告的方法就不一定能夠這樣。例如,下列程式碼會定義名為 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 提供函數參數一些功能,對剛開始使用此語言的程式設計人員會顯得很新奇。雖然以傳值或傳址的方式傳遞參數的概念對大部分程式設計人員而言應該很熟悉,但 arguments 物件及 ... (rest) 參數對很多人而言可能是新的概念。

以傳值或以傳址方式來傳遞引數

在許多程式語言中,瞭解以傳值或以傳址方式傳遞引數之間的區別相當重要,因為這項區別可能會影響程式碼設計的方式。

以傳值方式來傳遞表示時,引數的值會複製到區域變數內以便在函數中使用。以傳址方式來傳遞表示時,只會傳遞引數的參考而不會傳遞實際的值,也就是說不會製作實際引數的副本。這個方式實際會建立以引數形式傳遞的變數參考,並將其指定給區域變數以便在函數中使用。區域變數是函數之外的變數參考,讓您能夠變更原始的變數值。

在 ActionScript 3.0 中,所有引數都是以傳址方式傳遞,因為所有的值都會儲存為物件。但是,屬於基本資料類型的物件 (包括 Boolean、Number、int、uint 和 String) 都有特殊運算子,使它們能夠表現得像是以傳值方式傳遞。例如,下列程式碼會建立名為 passPrimitives() 的函數,以定義兩個同樣是 int 類型的參數:xParamyParam。這些參數類似於在 passPrimitives() 函數主體內部宣告的區域變數。當函數是以引數 xValueyValue 呼叫時,參數 xParamyParam 都是用以 xValueyValue 代表的 int 物件參考進行初始化。因為引數是基本型,所以表現得就像是以傳值方式傳遞一樣。雖然 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 物件 - 如果有任何參數命名為 arguments,或是如果使用 ... (rest) 參數。

    如果在函數主體中參考 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 所提供的功能。您應該先確認不需要使用 arguments.callee,之後再使用 ... (rest) 參數。

下列範例會使用 ... (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) 參數也可與其他參數一併使用,只要該參數是列出的最後一個參數即可。下列範例會修改 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 關鍵字的值則可變更。