Flash ActionScript (22)三天學透as3.0之第一天

1.類的由來

1.1 C 語言中的結構體
這部分屬於歷史問題,與技術無關,瞭解歷史可以讓我們更好地把握現在和將來。C 語言中的結構體 struct 可以說是類的最原始的雛形。只有 int, float, char 這些基本數據類型是不夠的,有時需要用不同的數據類型組合成一個有機的整體來使用。例如一個學生有學號和姓名就可以定義一個 Student 的結構體:
struct Student {
int id;
char[20] name;
} student;
main() {
// 可以使用“對象名.屬性” 的方式來操作數據
student.id = 5;
student.name = “ZhangSan”;
}

1.2 從結構體到類的演化(C —— C++)
C 中的結構體 C++ 中的結構體
struct 結構名 {
數據成員
}; struct 結構名 {
數據成員
成員函數
}

C++ 首次允許在結構體中定義成員函數!那麼再將 struct 關鍵字換成 class 不就是我們現在所看到的類的形態了嗎?
class Student {
private:
int id;
char[20] name;
public:
void gotoSchool() {}
}
C++ 最初的名字叫做“C with class”(帶類的C),經過長時間的發展,最終敲定將其命名爲 C++,“++”表示加一操作,代表它比 C 語言更進步,更高級。
面向過程的編程就是在處理一個個函數,而現在的面向對象編程處理是函數加數據,形式上就這麼點兒差別。也許剛剛接觸時它時會感到有些困難,這很正常。一旦你真正瞭解它,那你一定會愛上它。所以,請大家不要畏懼,技術永遠向着更方便,更簡單,更高效的方向發展,而不會向越來越難,越來越複雜發展。對於面向對象程序設計(OOP)而言,代表着越來越接近人類的自然語言,越來越接近人類的思維,因此一切都會變得越來越簡單。
從結構體到類的演變過程中我們看到,類中是可以定義函數的。因此,引出了面向對象三大特性之一,封裝。

2.封裝(Encapsulation)
2.1 封裝的概念
封裝的定義:把過程和數據包圍起來,對數據的訪問只能通過已定義的界面。在程序設計中,封裝是指將數據及對於這些數據有關的操作放在一起。
知道這些定義,並不能代表技術水平有多高。但是如果去參加面試也許會用得着。簡單解釋一下,它的意思是指把成員變量和成員函數放在一個類裏面,外面要想訪問該類的成員變量只能通過對外公開的成員函數來訪問。用戶不需要知道對象行爲的實現細節,只需根據對象提供的外部接口訪問對象即可。
這裏有一個原則叫做“信息隱藏”—— 通常應禁止直接訪問成員變量,而應該通過對外公開的接口來訪問。下面,看一個小例子:
class Father {
private var money:int = 10000000;
public takeMoney():int {
money -= 100;
return 100;
}
}
定義名爲 Father 的類(一個有錢的父親),類中有一個成員變量 money 它的訪問權限爲 private 意思是說 money 是 Father 私有的,private 是指只有在這個類裏面才能訪問,出了這個類就無法訪問該成員了,稍後會有關於訪問權限更多的解釋。
類中還定義了 takeMoney() 這個方法,它是 public 的,可以說是對外公開的一個接口。
從這個例子中可以看出,任何人要想從 Father 類取一些 money 的話,都只能通過 takeMoney() 這個方法去拿,而不能直接訪問 money 這個屬性,因爲它是私有的。只有通過 takeMoney() 這個公開的方法從能修改 Father 類的成員變量讓 money -= 100 —— 每次只能給你 100 元。對外只能看到 takeMoney() 這個方法,該方法如何實現的別人不知道,反正你每次只能得到 100 塊。

2.2 封裝的好處
封裝的好處:保證了模塊具有較高的獨立性,使得程序的維護和修改更加容易。對應程序的修改僅限於類的內部,將程序修改帶來的影響減少到最低。
 
2.3 封裝的目的
(1)隱藏類的實現細節;
(2)迫使用戶通過接口去訪問數據;
(3)增強代碼的可維護性。

2.4 封裝的技巧
按照純面向對象編程的思想,類中所有的成員變量都應該是 private 的,要操作這些私有的成員變量只能通過對外公開的函數來完成。實際工作中,有時也常把變量的訪問權限設置爲 public 目的就是爲了調用起來方便。在 AS 3 中提供了 get/set 關鍵字,它能讓我們以函數調用的方式處理屬性。按照封裝的原則,再結合 get/set 如何去操作一個成員變量呢?看下面這個例子:
class Person {
private var _age:int;

public function get age():int {
return _age;
}

public function set age(a:int):void {
if (a < 0) {
trace("age 不能小於 0!");
return;
}
_age = a;
}
}
首先,實例化出該類的對象 var person:Peron = new person()。如果要設置成員變量 _age 就要通過調用 set age() 來實現:person.age = 5。實際上,我們是在調用 set age(5) 這個方法,但是由於有了 set 修飾符,操作的方法就是給對象的屬性賦值,但實際上是在調用函數。但是如果這麼寫 person.age = -20,這樣合理嗎?一個人的年齡等於 -20 歲,顯然不對。因此在調用 set age() 方法中,可以進行一下判斷,如果給的參數不對就發出提示,這項工作只能由函數來完成,這是屬性沒辦法到的。
回想一下封裝的定義,它的用意就在於數據(成員變量)是核心,不可以隨隨便便去改,要想改只能去調用公開的函數,而在函數可以進行各種判斷,保證對數據的修改是經過思考的,合理的。
說到 get/set 現在去看看 ActionScript 3 是怎麼做的。我們知道 TextField 類有一個 text 屬性,查看幫助文檔,會看到:
 
text 屬性
實現
public function get text():String
public function set text(value:String):void
 
可見這個屬性也是通過 get/set 方法實現的,雖然我們是用“對象名.text”方式去操作 text 屬性,但實際上是在調用 get/set text(),那麼這兩個函數具體怎麼實現的呢?Who knows!這就叫隱藏實現。AS 3 裏面所有類的屬性都是這樣實現的,只有 get 沒有 set 的是隻讀屬性。
封裝時還要考慮,這個方法應該歸屬哪個類。首先,邏輯上要說得過去,例如製作一個貪吃蛇的遊戲,有一個方法叫 eat(),“吃”這個方法應該給誰?給文檔類?給食物?當然應該放到蛇這個類裏面,蛇去吃食物這才合理。然後,還要注意該方法要用到哪些屬性。例如,一個人拿粉筆在黑板上畫圓,那麼畫圓這個方法應該歸屬哪個類。人?黑板?粉筆?畫圓的時候需要知道圓的半徑和圓心,而這些屬性都在圓這個類裏面,所以畫圓這個方法應該放在圓這個類裏面。
下面,學習訪問控制修飾符。

3. 訪問控制(Access Control)
修飾符 類內可見 包內可見 子類可見 任何地方
private Yes
protected
Yes
Yes

internal(默認) Yes Yes
Yes
public Yes Yes Yes Yes

(1)private:僅當前類可訪問,稱爲私有成員;
(2)internal(默認):包內可見,在同一文件夾下可訪問(如果不寫權限修飾符,默認權限也是它);
(3)protected:子類可訪問,與包無關。如果不是該類的子類,那麼 protected 和 private 是一樣的;
(4)public:完全公開。任何地方、任何對象都可以訪問這個類的成員。

4. 繼承(Inheritance)
4.1 繼承的概念
下面進入面向對象的第二大特性 —— 繼承。
繼承的定義:繼承是一個類可以獲得另一個類的特性的機制,繼承支持層次概念。
繼承是一種代碼重用的形式,允許程序員基於現有類開發新類。現有類通常稱爲"基類"或"超類",新類通常稱爲"子類"或"派生類"。通過繼承還可以在代碼中利用多態。
繼承的好處:繼承最大的好處是代碼的重用,與它同樣重要的是它帶來了多態。關於多態後面會給大家非常詳細的講解,我們現在只討論繼承。
AS 3 和 Java 一樣都是單根繼承的。在 AS 3 中不論哪個類都有一個相同的唯一的老祖宗 —— Object。拿我們最熟悉的 Sprite 類來說,查看一下幫助文檔:
 
包 flash.display
類 public class Sprite
繼承 Sprite→DisplayObjectContainer→InteractiveObject→DisplayObject→EventDispatcher→Object
子類 FLVPlayback, FLVPlaybackCaptioning, MovieClip, UIComponent

從這裏可以清楚地看到,Sprite 的父類是 DisplayObjectContainer,而 DisplayObjectContainer 的父類是 InteractiveObject。一直往下找,最終都能夠找到 Object類,不僅是 Sprite,所有的類皆如此,這就叫單根繼承。AS 單根繼承的思想應該說是從 Java 借鑑來的,實踐證明單根繼承取得了很大的成功。Java 發明的單根繼承摒棄了 C++ 的多繼承所帶來的很多問題。
雖然在 AS 3 中不能直接利用多繼承,但是可以通過實現多個接口(interface)達到相同的目的,關於接口後面會單獨講解。單繼承和多繼承相比,無非多敲些代碼而已,但是它帶來的巨大好處是潛移默化的。具體有哪些好處這裏就不詳細討論了,總之是非常非常多,大家如果有興趣可以到網上搜索一下相關內容。
我們知道 Sprite 類有 x 和 y 屬性,現在請大家在幫助文檔中查找出 Sprite 類的這兩個屬性。你一定是先去打開 Sprite 這個類,但只看到五個屬性,裏面沒有我沒要找到 x, y 屬性!怎麼回事?這時候應該首先想到的是繼承,一定是它的父類裏面有,這兩個屬性是從父類中繼承過來的!OK,沒錯,就是這樣,那麼就去 DisplayObjectContainer 找找,可是還沒找到,再找父類的父類 InteractiveObject 還沒有,再找父類的父類的父類 DisplayObject 找到了吧。學會查幫助文檔非常重要,知道某個類有某個屬性或方法,如果在這個類中找不到就要去它的父類中去找,再找不到就去父類的父類中找,最遠到 Object 不信你找不到。
下面看一些例子。
4.2 TestPerson.as —— 屬性與方法的繼承
package {
public class TestStudent {
public function TestPerson() {
var student:Student = new Student();
student.name = "ZhangSan";
student.age = 18;
student.school = "114";
trace(student.name, student.age, student.school);
}
}
}
class Person {
private var _name:String;
private var _age:int;

public function get name():String{
return _name;
}

public function set name(n:String):void {
if (n == "") {
trace("name 不能爲空!");
return;
}
_name = n;
}

public function get age():int {
return _age;
}

public function set age(a:int):void {
if (a < 0) {
trace("age 不能小於 0!");
return;
}
_age = a;
}
}
class Student extends Person {
private var _school:String;

public function get school():String {
return _school;
}

public function set school(s:String):void {
_school = s;
}
}
注意,package 中包的是用來測試的類,爲了演示方便Person 和 Student 跟這個測試類放在了一個 as 文件中。
首先定義一個 Person 類,人都有名字(name)和年齡(age),下面讓 Student 類繼承 Person 類,也就是說學生繼承了人的所有特性,或者說“學生是一個人”,這話沒錯吧! “Student is a Person”,滿足 is-a,那麼就可以使用繼承。Student 在 Person 的基礎上還多出了 school 屬性,記錄着他所在學校的名稱。
在測試類中創建了一個 Student 對象,使用 student.name, student.age, student.school,設置學生的姓名,年齡和學校。雖然 Student 類中沒有定義 name, age 屬性,但這兩個屬性是它從父類 Person 繼承而來的,因此實現了代碼的複用,因此 Student 也就擁有了父類屬性和方法。

4.3 TestExtends.as —— 繼承的限制
package {
public class TestExtends {
public function TestExtends() {
new Son();
}
}
}
class Father {
private var money:int = 1000000;
public var car:String = "BMW";
private function work():void {
trace("writing");
}
}
class Son extends Father {
public var bike:String = "YongJiu";

// 沒有繼承因此談不上重寫
private function work():void {
trace("studying");
};

function Son (){
trace(bike);
trace(car);
//trace(money); // private 的屬性不能被繼承
work();
}
}
這個例子要演示哪些屬性或方法不能被繼承。本例中定義了一個 Father 類,其中包括 private 的money 屬性,public 的 car 屬性和 private 的 work() 方法。
然後定義一個 Son 類繼承自 Father 類,在 Son 類中新增加一個 bike 屬性,同樣也有一個 work() 方法。在 Son 的構造函數中打印出 bike, car屬性或調用 work() 方法都沒問題。但是不能打印出 money 屬性,因爲它是 Father 類私有的,回想一下第三節所講的“訪問控制”查一下那個表,可以看到 private 的成員只能在類內訪問,不能被繼承。那麼父類中還有一個 work() 方法也是 private 的,因此也不會被繼承,所以在子類中再定義一個 work() 方法也不會有衝突,也就談不上重寫。關於重寫,後面還有相關的例子。
學習 OOP 編程時,如果可以理解程序執行時內存的結構,那將對我們學習 OOP 編程有莫大好處。下面就來了解一下內存的結構以及各部分的作用。

4.4 內存分析

根據不同的操作系統內存的結構可能有所差異,但是通常在程序執行中會把內存分爲四部分:
 
(1)heap 堆:存放 new 出來的對象;
(2)stack 棧:局部變量;
(3)data segment 數據段:靜態變量和字符串常量;
(4)code segment 代碼段:存放代碼。
後面會在一些例子中利用內存圖來幫助理解。先看下面一個例子 this 和 super。

4.5 this 和 super 關鍵字
4.5.1 TestSuper.as —— this 和 super 以及 override
package {
public class TestSuper {
public function TestSuper() {
new Extender();
}
}
}
class Base {
public var name:String;
public var age:uint;

//function Base(){
// 即使不寫構造函數,系統也會自動添加的無參構造函數
//}

public function work():void {
trace("Writing!");
}
}
class Extender extends Base {
//public var name:String; // name 屬性已從父類繼承,不能重複定義

function Extender() {
// super(); // 即使不寫 super(),系統也會添加的無參的 super();
super.work();
this.work();
}

override public function work():void {
trace("Programming!");
}
}
這個例子演示了 this 與 super 關鍵字的使用,以及重寫(override)的概念。
首先,定義 Base 類代表基類,它被 Extender 類繼承。Base 類中定義兩個 public 的屬性 age 和 name,以及一個公有的方法 work()。Extender 類繼承了 Base 類的這些屬性和方法,但是如果要想在 Extender 類中再重複定義 name 或 age 屬性就不行了,因爲它已經繼承了這兩個屬性,不能再重複定義。但是方法可以被重新定義,前提是要在方法前面加上 override 關鍵字,顯示地說明要重寫(覆蓋)基類的 work() 方法。
這一樣來就有兩個 work() 方法瞭如何區分它們呢?我們可以在方法名前面顯示地加上 super 或 this 關鍵字,見 Extender 的構造函數。super.work() 表示調用父類的 work() 方法,this.work() 表示調用該類中的 work() 方法。
override 關鍵字表示對父類方法的重寫,override 是利用繼承實現多態的必要手段。
何時使用重寫呢?當我們對父類的方法不滿意、或者同樣的一個方法對於兩個類來說實現的功能不相同時,就要考慮重寫,把父類的方法沖掉,讓它改頭換面。
下面我們看一下內存圖中,this 和 super 各代表着什麼。

4.5.2 this 和 super 的內存分析
 
 

測試類中有一條語句 new Extender()。new 出來的對象放在堆內存(Heap Segment)中。在對象內部隱藏着兩個對象:this 和 super。
this 持有當前對象的引用;super 持有父類的引用。
上例中我們有兩種調用的方法:super.work() 和 this.work()。如果不加修飾,只寫 work() ,那麼系統默認調用的是 this.work()。
既然系統默認調用的就是 this.work(),那還要 this 有什麼用?比如,當局部變量與成員變量同名時,如果要在屬於這個局部變量的上下文中引用成員變量,那麼就要顯示地調用“this.成員變量”名來指定引用的是成員變量而非局部變量。請看下面一個例子。

4.5.3 TestThis.as —— 用 this 區分局部變量與成員變量
package {
public class TestThis {
public function TestThis() {
var p:Person = new Person();
p.setInfo("ZhangSan", 20);
}
}
}
class Person {
private var name:String;
private var age:int;

public function setInfo(name:String, age:int) {
this.name = name;
this.age = age;
}
}
本例中就是用 this.name 指定是成員變量 name,而非傳進來的局部變量的 name。age 也是如此。下面請看內存分析。
 

傳進去的兩個變量 name 和 age 屬於臨時變量存放在棧內存(Stack Segment),setInfo() 方法中的 this.name 和 this.age 則指的是該對象的成員變量 name 和 age。由於 name 和 age 都是基元數據類型,因此直接傳值,把 “zhang” 和 20 賦給了 Person 的成員變量。最後在 setInfo() 執行完成後,爲該方法分配的臨時變量全部釋放,賦值工作完成。
 

4.6 初始化順序
下面 Think in Java 中的一段演示代碼,見 TestSandwich.as:
package {
public class TestSandwich {
public function TestSandwich() {
new Sandwich();
}
}
}
class Water {
//static var w = trace("static water");
function Water() {
trace("Water");
}
}
class Meal {
//static var w = trace("static meal");
function Meal() {
trace("Meal");
}
}
class Bread {
function Bread() {
trace("Bread");
}
}
class Cheese {
function Cheese() {
trace("Cheese");
}
}
class Lettuce{
function Lettuce() {
trace("Lettuce");
}
}
class Lunch extends Meal {
function Lunch() {
trace("Lunch");
}
}
class PortableLunch extends Lunch {
//static var w = trace("static lunck");
function PortableLunch() {
trace("PortableLunch");
}
}
class Sandwich extends PortableLunch {
var bread:Bread = new Bread();
var cheese:Cheese = new Cheese();
var lettuce:Lettuce = new Lettuce();

//static var good = trace("static sandwich");

function Sandwich() {
trace("Sandwich");
}
}
測試類很簡單隻有一句:new Sandwich()。構造出 Sandwich 類一個實例。
Sandwich 類繼承了 PortableLunch 這個類。現在有一個問題,是先有子類還是先有父類?是先有父親後有兒子,還是先有兒子後有父親?肯定是先有父親。那麼怎麼有的父親?需要先構造出來。怎麼構造?調用構造函數!
因此,我們說在構造子類之前,要先將它的父類構造出來,如果父類還有父類,就要先把父類的父類構造出來。在這段程序中每個類在構造出來後都會打印出該類的類名。下面請看執行結果:
Bread
Cheese
Lettuce
Meal
Lunch
PortableLunch
Sandwich
我們看到,最先打印出來的是 Bread, Cheese, Lettuce。這是 Sandwich 類的三個成員變量,可見在調用構造函數之前,要新將該類的成員變量構造出來,然後再去構造這個類本身。
前面提到,要構造這個類就先要構造它的父類,Sandwich 的父類是 PortableLunch,而 PortableLunch 還有父類叫 Lunch,而 Lunch 還有父類叫 Meal,到了 Meal 就終止了。這時將執行 Meal 的構造函數,有了 Meal 之後就可以構造Lunch 了,有了 Lunch,PortableLunch 就可以構造,有了 PortableLunch 我們的 Sandwich 才被構造出來。
現在我們得到的結論就是:類的成員變量先被初始化,然後纔是構造函數。
下面,請大家把代碼中註釋掉的部分全部打開。現在,又新加入了一些 static 的成員變量,我們來實驗一下靜態的成員變量是何時被調用的,執行結果如下:
static water
static meal
static lunck
static sandwich
Bread
Cheese
Lettuce
Meal
Lunch
PortableLunch
Sandwich
我們看到,所有靜態的成員都先於非靜態成員變量被構造出來!最上面有一個 Water 類,雖然沒有地方會用到它,但是它也被打印出來了。結論是:當類被加載時靜態的屬性和方法就會被初始化。注意,什麼叫類被加載時?這是指類被加載到內存裏面。可見,我們整個這個 as 文件中的所有類都被加載到了內存中了,只要這個類被讀入到內存中,那麼它的所有靜態成員就會被初始化。
最終的結論 —— 初始化順序:
(1)當類被加載時該類的靜態的屬性和方法就會被初始化
(2)然後初始化成員變量
(3)最後構造出這個類本身
OK,既然這裏提到了靜態成員,下面我們就來了解一下它。

4.7 靜態屬性與方法
4.7.1 static的概念
靜態屬性與方法屬於這個類,而不屬於該類的實例。
靜態屬性只生成一份。同類對象的靜態屬性值都相同。改變一個類的靜態屬性值會影響該類所有的對象。靜態屬性可以節省內存,方便調用。一個方法即使不聲明爲靜態的實際上也只生成一份,因爲除了方法所處理的數據不同外,方法本身都相同的。
靜態屬性和靜態方法中不能存在非靜態的屬性和方法,或者說 static 的屬性和方法中不能出現 this。

4.7.2 TestStatic.as —— static 屬於這個類,不屬於該類實例
package {
public class TestStatic {
public function TestStatic() {
var b:Ball = new Ball();

// 靜態屬性或方法屬於這個類,而不屬於該類對象,只能用類名引用
// b.sMethod();
// trace(b.color);

Ball.sMethod();
trace(Ball.color);
}
}
}
class Ball {
public var xpos:Number = 300;
public static var color:uint = 0xff0000;
public function changeColor():void {
color = 0;
}

public static function sMethod():void {
trace("I'm a static Method");
//trace(xpos); // 靜態方法中不能調用非靜態成員變量
//changeColor(); // 靜態方法中不能調用非靜態成員函數
}
}
在 Ball 這個類中有一個靜態屬性 color,一個靜態方法 sMethod()。注意這條原則:靜態屬性與方法屬於這個類,而不屬於該類的實例。在 sMethod 中不能出現非靜態的成員變量或方法。反之可以,在非靜態的成員變量或方法中可以調用靜態的成員變量或方法。
注意,在測試類 TestStatic 中,要訪問靜態的成員變量或方法只能通過“類名.方法(或屬性)”的形式去調用,而不能通過“實例名.方法(或屬性)”的形式調用。
由於靜態屬性只生成一份,所有該類對象都共享這一個,因此可以節省一部分內存,並且可以一改全改。既然大家都用一份屬性,那就不能存在差別化了,這也是聲明靜態屬性的一個原則,當所有對象都共用一個相同的屬性時,可考慮將其聲明爲靜態屬性。而靜態方法帶來的好處就是方便引用。
下面,我們看看在設計模式中是如何使用 static 的,有請單例模式。

4.7.4 單例模式(Singleton Pattern)
單例模式是指“只能有一個該類的對象存在”。
單例模式有什麼用處?例如系統的緩存、註冊表、日誌、顯卡驅動、回收站等等都是獨一無二的,都只有一個。如果造出了多個實例,就會導致系統出問題。這時就需要用到單例模式,原則就是該類的對象只能有一個。
首先,如果一個類的構造函數是公開的,那麼就有可能被其它地方 new 出來,首先將構造函數變爲私有的。遺憾的是 ActionScript 3 中,構造函數只能是 public 的,不能爲 private。以下是 AS 3 版本的單例模式:
package {
public class Singleton {
static private var instance:Singleton;
public function Singleton(singletonEnforcer:SingletonEnforcer) {}
public static function getInstance():Singleton {
if (instance == null) {
instance = new Singleton(new SingletonEnforcer());
}
return instance;
}
}
}
class SingletonEnforcer { }
這裏用 SingletonEnforcer 類作爲 Singleton 類構造函數的參數,由於這兩個類放在一個 as 文件中,instance這個類只能被 Singleton 訪問到,保證其它類得不到 SingletonEnforcer 的實例,用這種方法達到私有構造函數的作用。
這就是單例模式,一個最簡單的設計模式!單例模式還可分爲餓漢式/懶漢式。上面演示的是懶漢式單例模式,它在第一次調用 getInstance() 方法時才 new 出這個對象來。沒人調用它的話,它永遠不會主動去做,是不是很懶?!另一種是餓漢式的,instance 一上來就 new 出來該類的實例。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章