轉(zhuǎn)自 : http://www.bjcan.com/hengxing/readlou.asp?id=1162

八、JavaScript面向?qū)ο蟮闹С?br />~~~~~~~~~~~~~~~~~~
(續(xù))

3. 構(gòu)造、析構(gòu)與原型問題
--------
?我們已經(jīng)知道一個對象是需要通過構(gòu)造器函數(shù)來產(chǎn)生的。我們先記住幾點:
?? - 構(gòu)造器是一個普通的函數(shù)
?? - 原型是一個對象實例
?? - 構(gòu)造器有原型屬性,對象實例沒有
?? - (如果正常地實現(xiàn)繼承模型,)對象實例的constructor屬性指向構(gòu)造器
?? - 從三、四條推出:obj.constructor.prototype指向該對象的原型

?好,我們接下來分析一個例子,來說明JavaScript的“繼承原型”聲明,以
及構(gòu)造過程。
//---------------------------------------------------------
// 理解原型、構(gòu)造、繼承的示例
//---------------------------------------------------------
function MyObject() {
? this.v1 = 'abc';
}

function MyObject2() {
? this.v2 = 'def';
}
MyObject2.prototype = new MyObject();

var obj1 = new MyObject();
var obj2 = new MyObject2();

?1). new()關(guān)鍵字的形式化代碼
?------
?我們先來看“obj1 = new MyObject()”這行代碼中的這個new關(guān)鍵字。

new關(guān)鍵字用于產(chǎn)生一個實例(說到這里補充一下,我習(xí)慣于把保留字叫關(guān)鍵字。
另外,在JavaScript中new關(guān)鍵字同時也是一個運算符),但這個實例應(yīng)當(dāng)是從
一個“原型的模板”復(fù)制過來的。這個用來作模板的原型對象,就是用“構(gòu)造器
函數(shù)的prototype屬性”所指向的那個對象。對于JavaScript“內(nèi)置對象的構(gòu)造
器”來說,它指向內(nèi)部的一個原型。

每一個函數(shù),無論它是否用作構(gòu)造器,都會有一個獨一無二的原型對象。缺省時
JavaScript用它構(gòu)造出一個“空的初始對象實例(不是null)”。然而如果你給函
數(shù)的這個prototype賦一個新的對象,那么構(gòu)造過程將用這個新對象作為“模板”。

接下來,構(gòu)造過程將調(diào)用MyObject()來完成初始化。——注意,這里只是“初始
化”。

為了清楚地解釋這個過程,我用代碼形式化地描述一下這個過程:
//---------------------------------------------------------
// new()關(guān)鍵字的形式化代碼
//---------------------------------------------------------
function new(aFunction) { // 如果有參數(shù)args
? var _this = aFunction.prototype.clone();? // 從prototype中復(fù)制一個對象
? aFunction.call(_this);??? // 調(diào)用構(gòu)造函數(shù)完成初始化, (如果有,)傳入args
? return _this;???????????? // 返回對象
}

所以我們看到以下兩點:
? - 構(gòu)造函數(shù)(aFunction)本身只是對傳入的this實例做“初始化”處理,而
??? 不是構(gòu)造一個對象實例。
? - 構(gòu)造的過程實際發(fā)生在new()關(guān)鍵字/運算符的內(nèi)部。

而且,構(gòu)造函數(shù)(aFunction)本身并不需要操作prototype,也不需要回傳this。


?2). 由用戶代碼維護的原型(prototype)鏈
?------
?接下來我們更深入的討論原型鏈與構(gòu)造過程的問題。這就是:
? - 原型鏈?zhǔn)怯脩舸a創(chuàng)建的,new()關(guān)鍵字并不協(xié)助維護原型鏈

以Delphi代碼為例,我們在聲明繼承關(guān)系的時候,可以用這樣的代碼:
//---------------------------------------------------------
// delphi中使用的“類”類型聲明
//---------------------------------------------------------
type
? TAnimal = class(TObject); // 動物
? TMammal = class(TAnimal); // 哺乳動物
? TCanine = class(TMammal); // 犬科的哺乳動物
? TDog = class(TCanine);??? // 狗

這時,Delphi的編譯器會通過編譯技術(shù)來維護一個繼承關(guān)系鏈表。我們可以通
過類似以下的代碼來查詢這個鏈表:
//---------------------------------------------------------
// delphi中使用繼關(guān)系鏈表的關(guān)鍵代碼
//---------------------------------------------------------
function isAnimal(obj: TObject): boolean;
begin
? Result := obj is TAnimal;
end;

var
? dog := TDog;

// ...
dog := TDog.Create();
writeln(isAnimal(dog));

可以看到,在Delphi的用戶代碼中,不需要直接繼護繼承關(guān)系的鏈表。這是因
為Delphi是強類型語言,在處理用class()關(guān)鍵字聲明類型時,delphi的編譯器
已經(jīng)為用戶構(gòu)造了這個繼承關(guān)系鏈。——注意,這個過程是聲明,而不是執(zhí)行
代碼。

而在JavaScript中,如果需要獲知對象“是否是某個基類的子類對象”,那么
你需要手工的來維護(與delphi這個例子類似的)一個鏈表。當(dāng)然,這個鏈有不
叫類型繼承樹,而叫“(對象的)原型鏈表”。——在JS中,沒有“類”類型。

參考前面的JS和Delphi代碼,一個類同的例子是這樣:
//---------------------------------------------------------
// JS中“原型鏈表”的關(guān)鍵代碼
//---------------------------------------------------------
// 1. 構(gòu)造器
function Animal() {};
function Mammal() {};
function Canine() {};
function Dog() {};

// 2. 原型鏈表
Mammal.prototype = new Animal();
Canine.prototype = new Mammal();
Dog.prototype = new Canine();

// 3. 示例函數(shù)
function isAnimal(obj) {
? return obj instanceof Animal;
}

var
? dog = new Dog();
document.writeln(isAnimal(dog));

可以看到,在JS的用戶代碼中,“原型鏈表”的構(gòu)建方法是一行代碼:
? "當(dāng)前類的構(gòu)造器函數(shù)".prototype = "直接父類的實例"

這與Delphi一類的語言不同:維護原型鏈的實質(zhì)是在執(zhí)行代碼,而非聲明。

那么,“是執(zhí)行而非聲明”到底有什么意義呢?

JavaScript是會有編譯過程的。這個過程主要處理的是“語法檢錯”、“語
法聲明”和“條件編譯指令”。而這里的“語法聲明”,主要處理的就是函
數(shù)聲明。——這也是我說“函數(shù)是第一類的,而對象不是”的一個原因。

如下例:
//---------------------------------------------------------
// 函數(shù)聲明與執(zhí)行語句的關(guān)系(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環(huán)境中執(zhí)行的結(jié)果是:
------------------------------------
? 1234
? undefined
? Exception: 'obj2' 未定義
? [object Object]
? hi, obj
------------------------------------
問題是,testFoo()是在它被聲明之前被執(zhí)行的;而同樣用“直接聲明”的
形式定義的object變量,卻不能在聲明之前引用。——例子中,第二、三
個輸入是不正確的。

函數(shù)可以在聲明之前引用,而其它類型的數(shù)值必須在聲明之后才能被使用。
這說明“聲明”與“執(zhí)行期引用”在JavaScript中是兩個過程。

另外我們也可以發(fā)現(xiàn),使用"var"來聲明的時候,編譯器會先確認(rèn)有該變量
存在,但變量的值會是“undefined”。——因此“testFoo(obj1)”不會發(fā)
生異常。但是,只有等到關(guān)于obj1的賦值語句被執(zhí)行過,才會有正常的輸出。
請對照第二、三與第四、五行輸出的差異。

由于JavaScript對原型鏈的維護是“執(zhí)行”而不是“聲明”,這說明“原型
鏈?zhǔn)怯捎脩舸a來維護的,而不是編譯器維護的。

由這個推論,我們來看下面這個例子:
//---------------------------------------------------------
// 示例:錯誤的原型鏈
//---------------------------------------------------------
// 1. 構(gòu)造器
function Animal() {}; // 動物
function Mammal() {}; // 哺乳動物
function Canine() {}; // 犬科的哺乳動物

// 2. 構(gòu)造原型鏈
var instance = new Mammal();
Mammal.prototype = new Animal();
Canine.prototype = instance;

// 3. 測試輸出
var obj = new Canine();
document.writeln(obj instanceof Animal);

這個輸出結(jié)果,使我們看到一個錯誤的原型鏈導(dǎo)致的結(jié)果“犬科的哺乳動
物‘不是’一種動物”。

根源在于“2. 構(gòu)造原型鏈”下面的幾行代碼是解釋執(zhí)行的,而不是象var和
function那樣是“聲明”并在編譯期被理解的。解決問題的方法是修改那三
行代碼,使得它的“執(zhí)行過程”符合邏輯:
//---------------------------------------------------------
// 上例的修正代碼(部分)
//---------------------------------------------------------
// 2. 構(gòu)造原型鏈
Mammal.prototype = new Animal();
var instance = new Mammal();
Canine.prototype = instance;


?3). 原型實例是如何被構(gòu)造過程使用的
?------
?仍以Delphi為例。構(gòu)造過程中,delphi中會首先創(chuàng)建一個指定實例大小的
“空的對象”,然后逐一給屬性賦值,以及調(diào)用構(gòu)造過程中的方法、觸發(fā)事
件等。這個過程跟JavaScript中的行為是一致的:
//---------------------------------------------------------
// JS中的構(gòu)造過程(形式代碼)
//---------------------------------------------------------
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();

如果以單個類為參考對象的,這個構(gòu)造過程中JavaScript可以擁有與Delphi
一樣豐富的行為。然而,由于Delphi中的構(gòu)造過程是“動態(tài)的”,因此事實上
Delphi還會調(diào)用父類(MyObject)的構(gòu)造過程,以及觸發(fā)父類的OnCreate()事件。

JavaScript沒有這樣的特性。父類的構(gòu)造過程僅僅發(fā)生在為原型(prototype
屬性)賦值的那一行代碼上。其后,無論有多少個new MyObject2()發(fā)生,
MyObject()這個構(gòu)造器都不會被使用。——這也意味著:
? - 構(gòu)造過程中,原型模板是一次性生成的;對這個原型實例的使用是不斷復(fù)
??? 制,而并不再調(diào)用原型的構(gòu)造器。

由于不再調(diào)用父類的構(gòu)造器,因此Delphi中的一些特性無法在JavaScript中實現(xiàn)。
這主要影響到構(gòu)造階段的一些事件和行為。——無法把一些“對象構(gòu)造過程中”
的代碼寫到父類的構(gòu)造器中。因為無論子類構(gòu)造多少次,這次對象的構(gòu)造過程根
本不會激活父類構(gòu)造器中的代碼。

所以再一次請大家看清楚new()關(guān)鍵字的形式代碼中的這一行:
//---------------------------------------------------------
// new()關(guān)鍵字的形式化代碼
//---------------------------------------------------------
function new(aFunction) { // 如果有參數(shù)args
? var _this = aFunction.prototype.clone(); // 從prototype中復(fù)制一個對象
? // ...
}

這個過程中,JavaScript做的是“prototype.clone()”,而Delphi等其它語言做
的是“Inherited Create()”。