進階主題

ActionScript 支援 OOP 的歷史背景

由於 ActionScript 3.0 是以舊版 ActionScript 為基礎所建立,瞭解 ActionScript 物件模型的演進情況可能會有幫助。ActionScript 最初是針對早期 Flash Professional 版本提供的簡單 Script 撰寫機制。後來,程式設計人員開始使用 ActionScript 建立日益複雜的應用程式。為了回應這些程式設計人員的需求,每一個後續版本都會有新增的語言功能,以便協助建立複雜的應用程式。

ActionScript 1.0

ActionScript 1.0 是用於 Flash Player 6 及更早版本中的語言版本。即使在這麼早期的開發階段,ActionScript 物件模型就是建立在以物件做為基礎資料類型的概念上。ActionScript 物件是具有一組「屬性」的複合資料類型;討論物件模型時,「屬性」一詞包含附加至物件的一切項目,如變數、函數或方法。

雖然這第一代 ActionScript 並不支援使用 class 關鍵字的類別定義,但可以使用稱為原型 (Prototype) 物件的特殊物件定義類別。ActionScript 1.0 原型語言跟 Java 和 C++ 類別語言不同:後兩者都使用 class 關鍵字建立抽象類別定義,以便實體化成具體物件,而 ActionScript 1.0 則是使用現有物件做為其它物件的模型 (或原型)。類別語言中的物件可以指向類別做為其範本,而原型語言中的物件卻是指向其它物件 (即其原型) 做為其範本。

若要在 ActionScript 1.0 中建立類別,必須為該類別定義建構函數。在 ActionScript 中,函數實際上就是物件,而不只是抽象定義。您所建立的建構函數是做為該類別之實體的原型物件。下列程式碼會建立名為 Shape 的類別,並定義一個名為 visible 的屬性,該屬性是預設為 true

// base class 
function Shape() {} 
// Create a property named visible. 
Shape.prototype.visible = true;

此建構函數會定義您可以用 new 運算子實體化的 Shape 類別,如下所示:

myShape = new Shape();

就像 Shape() 建構函數物件可以做為 Shape 類別的實體原型,它也可以做為 Shape 的子類別 (也就是擴充 Shape 類別的其它類別) 之原型。

建立 Shape 類別的子類別要執行兩步驟程序。首先,定義類別的建構函數以建立該類別,如下所示:

// child class 
function Circle(id, radius) 
{ 
this.id = id; 
this.radius = radius; 
}

接下來,使用 new 運算子,以宣告 Shape 類別為 Circle 類別的原型。根據預設,您所建立的任何類別都會使用 Object 類別做為其原型,也就是說, Circle.prototype 目前包含一般物件 (Object 類別的實體)。若要指定 Circle 的原型為 Shape 而不是 Object,請使用下列程式碼變更 Circle.prototype 的值,讓它包含 Shape 物件,而不是包含一般物件:

// Make Circle a subclass of Shape. 
Circle.prototype = new Shape();

Shape 類別和 Circle 類別現在會以繼承關係連結成一體,這關係一般稱為「原型鏈」。下圖可說明原型鏈中的關係:

每一個原型鏈尾端的基底類別都是 Object 類別。Object 類別包含名為 Object.prototype 的靜態屬性,它指向 ActionScript 1.0 建立之所有物件的基底原型物件。範例原型鏈中的下一個物件是 Shape 物件。這是因為 Shape.prototype 屬性從未明確地設定,因此它仍然保存一般物件 (Object 類別的實體)。這個鏈中最後一個連結是 Circle 類別,它是連結至其原型 - Shape 類別 ( Circle.prototype 原型保存了 Shape 物件)。

如果您建立 Circle 類別的實體 (如同下列範例一樣),此實體會繼承 Circle 類別的原型鏈:

// Create an instance of the Circle class. 
myCircle = new Circle();

不知您是否還記得,範例包含名為 visible 的屬性做為 Shape 類別的成員。在這個範例中, visible 屬性不是 myCircle 物件的一部分,而只是做為 Shape 物件的成員,然而下列程式碼行卻輸出 true

trace(myCircle.visible); // output: true

執行階段只要順著原型鏈往上走,就能查明是 myCircle 物件繼承了 visible 屬性。執行此程式碼時,執行階段會先搜尋 myCircle 物件的所有屬性,尋找名為 visible 的屬性,但找不到這個屬性。接著,它就在 Circle.prototype 物件中尋找,但還是找不到名為 visible 的屬性。繼續順著原型鏈往上走,它終於找到在 Shape.prototype 物件上定義的 visible 屬性,並輸出該屬性的值。

由於想要盡可能地簡單明瞭,因此本節內容省略了原型鏈的許多細節和錯綜複雜的關係。而是將重點放在提供足夠資訊,希望能協助您瞭解 ActionScript 3.0 物件模型。

ActionScript 2.0

ActionScript 2.0 導入新的關鍵字,如 class extends public private ,可讓您以使用 Java 和 C++ 等類別語言者所熟悉的方式來定義類別。請務必要了解,基礎繼承機制在 ActionScript 1.0 與 ActionScript 2.0 之間並沒有任何改變,ActionScript 2.0 只是加了新語法,可供定義類別而已。原型鏈在這兩版語言中的運作方式完全一樣。

由 ActionScript 2.0 導入的新語法 (如下列摘錄所示),讓您能夠以許多程式設計人員覺得更為直覺的方式來定義類別:

// base class 
class Shape 
{ 
var visible:Boolean = true; 
}

請注意,ActionScript 2.0 也導入類型附註,可用來進行編譯階段類型檢查。如此一來,您便可以宣告上一個範例中的 visible 屬性應該只包含 Boolean 值;新的 extends 關鍵字也簡化了建立子類別的程序。在下列範例中,ActionScript 1.0 中必須要有兩個步驟的程序,而使用 extends 關鍵字只需一個步驟就可完成:

// child class 
class Circle extends Shape 
{ 
    var id:Number; 
    var radius:Number; 
    function Circle(id, radius) 
    { 
        this.id = id; 
        this.radius = radius; 
        } 
}

建構函式現在是宣告為類別定義的一部分,而類別屬性 id radius 都必須明確地進行宣告。

ActionScript 2.0 也新增介面定義的支援,可讓您利用正式為物件間通訊定義的通訊協定,更進一步修改您的物件導向程式。

ActionScript 3.0 類別物件

常見的物件導向程式設計範式通常是與 Java 和 C++ 相關聯,會使用類別來定義物件的類型。採用這種範式的程式設計語言也傾向於使用類別來建構類別所定義資料類型的實體。ActionScript 同時將類別用於這兩種用途,不僅紮根於原型語言,更增添了值得玩味的特性。ActionScript 為每一個定義建立特殊的類別物件,而允許同時共用行為和狀態。但是對許多 ActionScript 程式設計人員來說,這項區別沒有實際的程式含意。ActionScript 3.0 的設計方式能讓您建立更複雜的物件導向 ActionScript 應用程式,而不需要使用 (甚至也不需要瞭解) 這些特殊類別物件。

下圖會顯示類別物件的結構,它代表名為 A 而用陳述式 class A {} 定義的簡單類別:

圖中的每一個矩形都代表一個物件。圖中每一個物件都有下標字元 A,以代表屬於類別 A,類別物件 (CA) 包含其它重要物件的數目參考。實體特性物件 (TA) 會儲存在類別定義之內定義的實體屬性。類別特性物件 (TCA) 代表類別的內部類型,並儲存由類別定義的靜態屬性 (下標字元 C 代表「類別」)。原型物件 (PA) 永遠都是指原先透過 constructor 屬性附加其上的類別物件。

特性物件

特性物件是 ActionScript 3.0 中的新增物件,實作時會以效能為考量。在舊版 ActionScript 中,名稱查閱程序可能會相當耗費時間,因為 Flash Player 會順著原型鏈查詢;在 ActionScript 3.0 中,名稱查閱會更有效率,所耗費時間較少,因為繼承的屬性是從父類別往下複製,一直到子類別的特性物件。

特性物件不能直接由程式設計人員編寫的程式碼存取,但可從效能改善及記憶體使用情形感覺到它的存在。特性物件提供 AVM2 有關類別版面及內容的詳細資訊。掌握了這些資訊之後,AVM2 就能大幅減少執行時間,因為它可以經常直接由機器產生指示以存取屬性,或是直接呼叫方法,而不需進行耗費時間的名稱查閱。

使用了特性物件的話,一個物件的記憶體使用量便會比舊版 ActionScript 中的類似物件減少相當多。例如,若是類別被密封 (也就是,類別不是動態宣告),類別的實體就不需要供動態加入屬性使用的雜湊表,而且所包含的只是特性物件的指標加上一些類別中所定義固定屬性的空間位置而已。其結果就是,在 ActionScript 2.0 中需要 100 位元組記憶體的物件,在 ActionScript 3.0 中可能只需要 20 位元組左右的記憶體就夠了。

備註: 特性物件是內部實作詳細資訊,並不保證在將來新版 ActionScript 中不會改變或甚至完全消失。

原型物件

每個 ActionScript 類別物件都有名為 prototype 的屬性,是類別之原型物件的參考,原型物件是舊版 ActionScript 原型語言根源留下的產物。如需詳細資訊,請參閱ActionScript 支援 OOP 的歷史背景。

prototype 屬性是唯讀屬性,也就是不能進行修改以另外指向不同的物件。這與舊版 ActionScript 中的類別 prototype 屬性不同,在舊版中,原型可以重新指定,以指向不同的類別。雖然 prototype 屬性是唯讀性質,但所參考原型物件並不是唯讀的,換句話說,新的屬性可以加入至原型物件中,加入原型物件的屬性可以供類別中所有實體共用。

原型鏈是舊版 ActionScript 中的唯一繼承機制,但在 ActionScript 3.0 中則只是扮演了次要的角色,主要的繼承機制為固定的屬性繼承,是由特性物件在內部進行處理。固定的屬性是定義為類別定義一部分的變數或方法。固定的屬性繼承也稱為類別繼承,因為它是與關鍵字 class extends override 等關聯的繼承機制。

原型鏈提供另外一種形式的繼承機制,比固定的屬性繼承更具彈性。不但可以將屬性做為類別定義的一部分加入至類別的原型物件,也可以在執行階段透過類別物件的 prototype 屬性加入。但請注意,如果將編譯器設定為嚴謹模式,可能就無法存取加入至原型物件的屬性,除非以 dynamic 關鍵字進行宣告。

Object 類別是有某些屬性附加至原型物件之類別的好範例。Object 類別的 toString() valueOf() 方法其實都是指定給 Object 類別之原型物件屬性的函數。下列範例示範這些方法的宣告在理論上的情況 (實際實作會因為實作細節的關係而稍微有些差異):

public dynamic class Object 
{ 
    prototype.toString = function() 
    { 
        // statements 
    }; 
    prototype.valueOf = function() 
    { 
        // statements 
    }; 
}

上文已經討論過,您可以在類別定義之外,將屬性附加至類別的原型物件。例如, toString() 方法也可以在 Object 類別定義之外定義,如下所示:

Object.prototype.toString = function() 
{ 
    // statements 
};

但是,原型繼承與固定的屬性繼承不同,若要在子類別中重新定義方法,並不需要 override 關鍵字。例如:若要在 Object 類別的子類別中重新定義 valueOf() 方法,您有三種選擇:第一,您可以在類別定義中的子類別之原型物件上定義 valueOf() 方法。下列程式碼會建立名為 Foo 的 Object 之子類別,然後在 Foo 的原型物件上重新定義 valueOf() 方法,做為類別定義的一部分。由於每一個物件都是繼承自 Object,所以並不需要使用 extends 關鍵字。

dynamic class Foo 
{ 
    prototype.valueOf = function() 
    { 
        return "Instance of Foo"; 
    }; 
}

第二,您可以在類別定義之外,於 Foo 的原型物件上定義 valueOf() 方法,如下列程式碼所示:

Foo.prototype.valueOf = function() 
{ 
    return "Instance of Foo"; 
};

第三,您可以定義名為 valueOf() 的固定屬性做為 Foo 類別的一部分。這種技巧與其它方法都不同,它混合了固定的屬性繼承與原型繼承。任何要重新定義 valueOf() 的 Foo 子類別都必須使用 override 關鍵字。下列程式碼示範如何將 valueOf() 定義為 Foo 中的固定屬性:

class Foo 
{ 
    function valueOf():String 
    { 
        return "Instance of Foo"; 
    } 
}

AS3 命名空間

兩種不同繼承機制 (固定的屬性繼承和原型繼承) 的存在,造就了在核心類別的屬性及方法方面的相容性挑戰。與 ECMAScript 語言規格 (ActionScript 的撰寫依據) 的相容性需要使用原型繼承,也就是說,核心類別的屬性和方法都是在該類別的原型物件上定義。另一方面,與 ActionScript 3.0 的相容性則需要使用固定的屬性繼承,也就是說,核心類別的屬性和方法是使用 const var function 等關鍵字在類別定義中定義。而且,使用固定的屬性而不使用原型版本,可能會讓執行階段效能大幅提升。

ActionScript 3.0 是透過同時使用核心類別的原型繼承和固定屬性繼承,來解決這個問題。每一個核心類別都包含兩組屬性和方法:一組是在原型物件上為 ECMAScript 規格之相容性所定義,另一組則是以固定的屬性和 AS3 命名空間,為 ActionScript 3.0 之相容性所定義。

AS3 命名空間提供在兩組屬性和方法之間選擇的便利機制。若不使用 AS3 命名空間,核心類別的實體會繼承在核心類別之原型物件上定義的屬性和方法。若決定使用 AS3 命名空間,則核心類別的實體會繼承 AS3 版,因為固定的屬性總是比原型屬性優先繼承。換句話說,只要可以使用固定的屬性,一定會使用此屬性,而不使用名稱完全相同的原型屬性。

您可以透過用 AS3 命名空間加以限定,選擇性地使用 AS3 命名空間版屬性或方法。例如,下列程式碼會使用 AS3 版 Array.pop() 方法:

var nums:Array = new Array(1, 2, 3); 
nums.AS3::pop(); 
trace(nums); // output: 1,2

另外,您也可以使用 use namespace 指令,開啟在程式碼區塊之內所有定義的 AS3 命名空間。例如,下列程式碼會使用 use namespace 指令,同時開啟 pop() push() 方法的 AS3 命名空間:

use namespace AS3; 
 
var nums:Array = new Array(1, 2, 3); 
nums.pop(); 
nums.push(5); 
trace(nums) // output: 1,2,5

ActionScript 3.0 也為各組屬性提供編譯器選項,以便讓您將 AS3 命名空間套用至整個程式。 -as3 編譯器選項代表 AS3 命名空間,而 -es 編譯器選項則代表原型繼承選項 ( es 代表 ECMAScript)。若要為整個程式開啟 AS3 命名空間,請將 -as3 編譯器選項設定為 true ,而將 -es 編譯器選項設定為 false 。若要使用原型版本,請將編譯器選項設定為相反的值。Flash Builder 和 Flash Professional 的預設編譯器設定是 -as3 = true -es = false

若計劃擴充任何核心類別及覆寫任何方法,應該要瞭解 AS3 命名空間會如何影響您必須宣告被覆寫方法的方式。若要使用 AS3 命名空間,核心類別的任何方法覆寫也必須使用 AS3 命名空間,以及 override 特質。若不要使用 AS3 命名空間,而要在子類別中重新定義核心類別方法,就不應該使用 AS3 命名空間或 override 關鍵字。