轉自 : http://www.bjcan.com/hengxing/readlou.asp?id=1162
八、JavaScript面向對象的支持
~~~~~~~~~~~~~~~~~~
(續)
3. 構造、析構與原型問題
--------
?我們已經知道一個對象是需要通過構造器函數來產生的。我們先記住幾點:
?? - 構造器是一個普通的函數
?? - 原型是一個對象實例
?? - 構造器有原型屬性,對象實例沒有
?? - (如果正常地實現繼承模型,)對象實例的constructor屬性指向構造器
?? - 從三、四條推出:obj.constructor.prototype指向該對象的原型
?好,我們接下來分析一個例子,來說明JavaScript的“繼承原型”聲明,以
及構造過程。
//---------------------------------------------------------
// 理解原型、構造、繼承的示例
//---------------------------------------------------------
function MyObject() {
? this.v1 = 'abc';
}
function MyObject2() {
? this.v2 = 'def';
}
MyObject2.prototype = new MyObject();
var obj1 = new MyObject();
var obj2 = new MyObject2();
?1). new()關鍵字的形式化代碼
?------
?我們先來看“obj1 = new MyObject()”這行代碼中的這個new關鍵字。
new關鍵字用于產生一個實例(說到這里補充一下,我習慣于把保留字叫關鍵字。
另外,在JavaScript中new關鍵字同時也是一個運算符),但這個實例應當是從
一個“原型的模板”復制過來的。這個用來作模板的原型對象,就是用“構造器
函數的prototype屬性”所指向的那個對象。對于JavaScript“內置對象的構造
器”來說,它指向內部的一個原型。
每一個函數,無論它是否用作構造器,都會有一個獨一無二的原型對象。缺省時
JavaScript用它構造出一個“空的初始對象實例(不是null)”。然而如果你給函
數的這個prototype賦一個新的對象,那么構造過程將用這個新對象作為“模板”。
接下來,構造過程將調用MyObject()來完成初始化。——注意,這里只是“初始
化”。
為了清楚地解釋這個過程,我用代碼形式化地描述一下這個過程:
//---------------------------------------------------------
// new()關鍵字的形式化代碼
//---------------------------------------------------------
function new(aFunction) { // 如果有參數args
? var _this = aFunction.prototype.clone();? // 從prototype中復制一個對象
? aFunction.call(_this);??? // 調用構造函數完成初始化, (如果有,)傳入args
? return _this;???????????? // 返回對象
}
所以我們看到以下兩點:
? - 構造函數(aFunction)本身只是對傳入的this實例做“初始化”處理,而
??? 不是構造一個對象實例。
? - 構造的過程實際發生在new()關鍵字/運算符的內部。
而且,構造函數(aFunction)本身并不需要操作prototype,也不需要回傳this。
?2). 由用戶代碼維護的原型(prototype)鏈
?------
?接下來我們更深入的討論原型鏈與構造過程的問題。這就是:
? - 原型鏈是用戶代碼創建的,new()關鍵字并不協助維護原型鏈
以Delphi代碼為例,我們在聲明繼承關系的時候,可以用這樣的代碼:
//---------------------------------------------------------
// delphi中使用的“類”類型聲明
//---------------------------------------------------------
type
? TAnimal = class(TObject); // 動物
? TMammal = class(TAnimal); // 哺乳動物
? TCanine = class(TMammal); // 犬科的哺乳動物
? TDog = class(TCanine);??? // 狗
這時,Delphi的編譯器會通過編譯技術來維護一個繼承關系鏈表。我們可以通
過類似以下的代碼來查詢這個鏈表:
//---------------------------------------------------------
// delphi中使用繼關系鏈表的關鍵代碼
//---------------------------------------------------------
function isAnimal(obj: TObject): boolean;
begin
? Result := obj is TAnimal;
end;
var
? dog := TDog;
// ...
dog := TDog.Create();
writeln(isAnimal(dog));
可以看到,在Delphi的用戶代碼中,不需要直接繼護繼承關系的鏈表。這是因
為Delphi是強類型語言,在處理用class()關鍵字聲明類型時,delphi的編譯器
已經為用戶構造了這個繼承關系鏈。——注意,這個過程是聲明,而不是執行
代碼。
而在JavaScript中,如果需要獲知對象“是否是某個基類的子類對象”,那么
你需要手工的來維護(與delphi這個例子類似的)一個鏈表。當然,這個鏈有不
叫類型繼承樹,而叫“(對象的)原型鏈表”。——在JS中,沒有“類”類型。
參考前面的JS和Delphi代碼,一個類同的例子是這樣:
//---------------------------------------------------------
// JS中“原型鏈表”的關鍵代碼
//---------------------------------------------------------
// 1. 構造器
function Animal() {};
function Mammal() {};
function Canine() {};
function Dog() {};
// 2. 原型鏈表
Mammal.prototype = new Animal();
Canine.prototype = new Mammal();
Dog.prototype = new Canine();
// 3. 示例函數
function isAnimal(obj) {
? return obj instanceof Animal;
}
var
? dog = new Dog();
document.writeln(isAnimal(dog));
可以看到,在JS的用戶代碼中,“原型鏈表”的構建方法是一行代碼:
? "當前類的構造器函數".prototype = "直接父類的實例"
這與Delphi一類的語言不同:維護原型鏈的實質是在執行代碼,而非聲明。
那么,“是執行而非聲明”到底有什么意義呢?
JavaScript是會有編譯過程的。這個過程主要處理的是“語法檢錯”、“語
法聲明”和“條件編譯指令”。而這里的“語法聲明”,主要處理的就是函
數聲明。——這也是我說“函數是第一類的,而對象不是”的一個原因。
如下例:
//---------------------------------------------------------
// 函數聲明與執行語句的關系(firefox 兼容)
//---------------------------------------------------------
// 1. 輸出1234
testFoo(1234);
// 2. 嘗試輸出obj1
// 3. 嘗試輸出obj2
testFoo(obj1);
try {
? testFoo(obj2);
}
catch(e) {
? document.writeln('Exception: ', e.description, '<BR>');
}
// 聲明testFoo()
function testFoo(v) {
? document.writeln(v, '<BR>');
}
//? 聲明object
var obj1 = {};
obj2 = {
? toString: function() {return 'hi, object.'}
}
// 4. 輸出obj1
// 5. 輸出obj2
testFoo(obj1);
testFoo(obj2);
這個示例代碼在JS環境中執行的結果是:
------------------------------------
? 1234
? undefined
? Exception: 'obj2' 未定義
? [object Object]
? hi, obj
------------------------------------
問題是,testFoo()是在它被聲明之前被執行的;而同樣用“直接聲明”的
形式定義的object變量,卻不能在聲明之前引用。——例子中,第二、三
個輸入是不正確的。
函數可以在聲明之前引用,而其它類型的數值必須在聲明之后才能被使用。
這說明“聲明”與“執行期引用”在JavaScript中是兩個過程。
另外我們也可以發現,使用"var"來聲明的時候,編譯器會先確認有該變量
存在,但變量的值會是“undefined”。——因此“testFoo(obj1)”不會發
生異常。但是,只有等到關于obj1的賦值語句被執行過,才會有正常的輸出。
請對照第二、三與第四、五行輸出的差異。
由于JavaScript對原型鏈的維護是“執行”而不是“聲明”,這說明“原型
鏈是由用戶代碼來維護的,而不是編譯器維護的。
由這個推論,我們來看下面這個例子:
//---------------------------------------------------------
// 示例:錯誤的原型鏈
//---------------------------------------------------------
// 1. 構造器
function Animal() {}; // 動物
function Mammal() {}; // 哺乳動物
function Canine() {}; // 犬科的哺乳動物
// 2. 構造原型鏈
var instance = new Mammal();
Mammal.prototype = new Animal();
Canine.prototype = instance;
// 3. 測試輸出
var obj = new Canine();
document.writeln(obj instanceof Animal);
這個輸出結果,使我們看到一個錯誤的原型鏈導致的結果“犬科的哺乳動
物‘不是’一種動物”。
根源在于“2. 構造原型鏈”下面的幾行代碼是解釋執行的,而不是象var和
function那樣是“聲明”并在編譯期被理解的。解決問題的方法是修改那三
行代碼,使得它的“執行過程”符合邏輯:
//---------------------------------------------------------
// 上例的修正代碼(部分)
//---------------------------------------------------------
// 2. 構造原型鏈
Mammal.prototype = new Animal();
var instance = new Mammal();
Canine.prototype = instance;
?3). 原型實例是如何被構造過程使用的
?------
?仍以Delphi為例。構造過程中,delphi中會首先創建一個指定實例大小的
“空的對象”,然后逐一給屬性賦值,以及調用構造過程中的方法、觸發事
件等。這個過程跟JavaScript中的行為是一致的:
//---------------------------------------------------------
// JS中的構造過程(形式代碼)
//---------------------------------------------------------
function MyObject2() {
? this.prop = 3;
? this.method = a_method_function;
? if (you_want) {
??? this.method();
??? this.fire_OnCreate();
? }
}
MyObject2.prototype = new MyObject(); // MyObject()的聲明略
var obj = new MyObject2();
如果以單個類為參考對象的,這個構造過程中JavaScript可以擁有與Delphi
一樣豐富的行為。然而,由于Delphi中的構造過程是“動態的”,因此事實上
Delphi還會調用父類(MyObject)的構造過程,以及觸發父類的OnCreate()事件。
JavaScript沒有這樣的特性。父類的構造過程僅僅發生在為原型(prototype
屬性)賦值的那一行代碼上。其后,無論有多少個new MyObject2()發生,
MyObject()這個構造器都不會被使用。——這也意味著:
? - 構造過程中,原型模板是一次性生成的;對這個原型實例的使用是不斷復
??? 制,而并不再調用原型的構造器。
由于不再調用父類的構造器,因此Delphi中的一些特性無法在JavaScript中實現。
這主要影響到構造階段的一些事件和行為。——無法把一些“對象構造過程中”
的代碼寫到父類的構造器中。因為無論子類構造多少次,這次對象的構造過程根
本不會激活父類構造器中的代碼。
所以再一次請大家看清楚new()關鍵字的形式代碼中的這一行:
//---------------------------------------------------------
// new()關鍵字的形式化代碼
//---------------------------------------------------------
function new(aFunction) { // 如果有參數args
? var _this = aFunction.prototype.clone(); // 從prototype中復制一個對象
? // ...
}
這個過程中,JavaScript做的是“prototype.clone()”,而Delphi等其它語言做
的是“Inherited Create()”。