轉(zhuǎn)自 : http://www.bjcan.com/hengxing/readlou.asp?id=1162
八、JavaScript面向?qū)ο蟮闹С?br />~~~~~~~~~~~~~~~~~~
(續(xù))
3. 構(gòu)造、析構(gòu)與原型問(wèn)題
--------
?我們已經(jīng)知道一個(gè)對(duì)象是需要通過(guò)構(gòu)造器函數(shù)來(lái)產(chǎn)生的。我們先記住幾點(diǎn):
?? - 構(gòu)造器是一個(gè)普通的函數(shù)
?? - 原型是一個(gè)對(duì)象實(shí)例
?? - 構(gòu)造器有原型屬性,對(duì)象實(shí)例沒(méi)有
?? - (如果正常地實(shí)現(xiàn)繼承模型,)對(duì)象實(shí)例的constructor屬性指向構(gòu)造器
?? - 從三、四條推出:obj.constructor.prototype指向該對(duì)象的原型
?好,我們接下來(lái)分析一個(gè)例子,來(lái)說(shuō)明JavaScript的“繼承原型”聲明,以
及構(gòu)造過(guò)程。
//---------------------------------------------------------
// 理解原型、構(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)鍵字的形式化代碼
?------
?我們先來(lái)看“obj1 = new MyObject()”這行代碼中的這個(gè)new關(guān)鍵字。
new關(guān)鍵字用于產(chǎn)生一個(gè)實(shí)例(說(shuō)到這里補(bǔ)充一下,我習(xí)慣于把保留字叫關(guān)鍵字。
另外,在JavaScript中new關(guān)鍵字同時(shí)也是一個(gè)運(yùn)算符),但這個(gè)實(shí)例應(yīng)當(dāng)是從
一個(gè)“原型的模板”復(fù)制過(guò)來(lái)的。這個(gè)用來(lái)作模板的原型對(duì)象,就是用“構(gòu)造器
函數(shù)的prototype屬性”所指向的那個(gè)對(duì)象。對(duì)于JavaScript“內(nèi)置對(duì)象的構(gòu)造
器”來(lái)說(shuō),它指向內(nèi)部的一個(gè)原型。
每一個(gè)函數(shù),無(wú)論它是否用作構(gòu)造器,都會(huì)有一個(gè)獨(dú)一無(wú)二的原型對(duì)象。缺省時(shí)
JavaScript用它構(gòu)造出一個(gè)“空的初始對(duì)象實(shí)例(不是null)”。然而如果你給函
數(shù)的這個(gè)prototype賦一個(gè)新的對(duì)象,那么構(gòu)造過(guò)程將用這個(gè)新對(duì)象作為“模板”。
接下來(lái),構(gòu)造過(guò)程將調(diào)用MyObject()來(lái)完成初始化?!⒁猓@里只是“初始
化”。
為了清楚地解釋這個(gè)過(guò)程,我用代碼形式化地描述一下這個(gè)過(guò)程:
//---------------------------------------------------------
// new()關(guān)鍵字的形式化代碼
//---------------------------------------------------------
function new(aFunction) { // 如果有參數(shù)args
? var _this = aFunction.prototype.clone();? // 從prototype中復(fù)制一個(gè)對(duì)象
? aFunction.call(_this);??? // 調(diào)用構(gòu)造函數(shù)完成初始化, (如果有,)傳入args
? return _this;???????????? // 返回對(duì)象
}
所以我們看到以下兩點(diǎn):
? - 構(gòu)造函數(shù)(aFunction)本身只是對(duì)傳入的this實(shí)例做“初始化”處理,而
??? 不是構(gòu)造一個(gè)對(duì)象實(shí)例。
? - 構(gòu)造的過(guò)程實(shí)際發(fā)生在new()關(guān)鍵字/運(yùn)算符的內(nèi)部。
而且,構(gòu)造函數(shù)(aFunction)本身并不需要操作prototype,也不需要回傳this。
?2). 由用戶代碼維護(hù)的原型(prototype)鏈
?------
?接下來(lái)我們更深入的討論原型鏈與構(gòu)造過(guò)程的問(wèn)題。這就是:
? - 原型鏈?zhǔn)怯脩舸a創(chuàng)建的,new()關(guān)鍵字并不協(xié)助維護(hù)原型鏈
以Delphi代碼為例,我們?cè)诼暶骼^承關(guān)系的時(shí)候,可以用這樣的代碼:
//---------------------------------------------------------
// delphi中使用的“類”類型聲明
//---------------------------------------------------------
type
? TAnimal = class(TObject); // 動(dòng)物
? TMammal = class(TAnimal); // 哺乳動(dòng)物
? TCanine = class(TMammal); // 犬科的哺乳動(dòng)物
? TDog = class(TCanine);??? // 狗
這時(shí),Delphi的編譯器會(huì)通過(guò)編譯技術(shù)來(lái)維護(hù)一個(gè)繼承關(guān)系鏈表。我們可以通
過(guò)類似以下的代碼來(lái)查詢這個(gè)鏈表:
//---------------------------------------------------------
// 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的用戶代碼中,不需要直接繼護(hù)繼承關(guān)系的鏈表。這是因
為Delphi是強(qiáng)類型語(yǔ)言,在處理用class()關(guān)鍵字聲明類型時(shí),delphi的編譯器
已經(jīng)為用戶構(gòu)造了這個(gè)繼承關(guān)系鏈?!⒁?,這個(gè)過(guò)程是聲明,而不是執(zhí)行
代碼。
而在JavaScript中,如果需要獲知對(duì)象“是否是某個(gè)基類的子類對(duì)象”,那么
你需要手工的來(lái)維護(hù)(與delphi這個(gè)例子類似的)一個(gè)鏈表。當(dāng)然,這個(gè)鏈有不
叫類型繼承樹(shù),而叫“(對(duì)象的)原型鏈表”?!贘S中,沒(méi)有“類”類型。
參考前面的JS和Delphi代碼,一個(gè)類同的例子是這樣:
//---------------------------------------------------------
// 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 = "直接父類的實(shí)例"
這與Delphi一類的語(yǔ)言不同:維護(hù)原型鏈的實(shí)質(zhì)是在執(zhí)行代碼,而非聲明。
那么,“是執(zhí)行而非聲明”到底有什么意義呢?
JavaScript是會(huì)有編譯過(guò)程的。這個(gè)過(guò)程主要處理的是“語(yǔ)法檢錯(cuò)”、“語(yǔ)
法聲明”和“條件編譯指令”。而這里的“語(yǔ)法聲明”,主要處理的就是函
數(shù)聲明?!@也是我說(shuō)“函數(shù)是第一類的,而對(duì)象不是”的一個(gè)原因。
如下例:
//---------------------------------------------------------
// 函數(shù)聲明與執(zhí)行語(yǔ)句的關(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);
這個(gè)示例代碼在JS環(huán)境中執(zhí)行的結(jié)果是:
------------------------------------
? 1234
? undefined
? Exception: 'obj2' 未定義
? [object Object]
? hi, obj
------------------------------------
問(wèn)題是,testFoo()是在它被聲明之前被執(zhí)行的;而同樣用“直接聲明”的
形式定義的object變量,卻不能在聲明之前引用?!又?,第二、三
個(gè)輸入是不正確的。
函數(shù)可以在聲明之前引用,而其它類型的數(shù)值必須在聲明之后才能被使用。
這說(shuō)明“聲明”與“執(zhí)行期引用”在JavaScript中是兩個(gè)過(guò)程。
另外我們也可以發(fā)現(xiàn),使用"var"來(lái)聲明的時(shí)候,編譯器會(huì)先確認(rèn)有該變量
存在,但變量的值會(huì)是“undefined”?!虼恕皌estFoo(obj1)”不會(huì)發(fā)
生異常。但是,只有等到關(guān)于obj1的賦值語(yǔ)句被執(zhí)行過(guò),才會(huì)有正常的輸出。
請(qǐng)對(duì)照第二、三與第四、五行輸出的差異。
由于JavaScript對(duì)原型鏈的維護(hù)是“執(zhí)行”而不是“聲明”,這說(shuō)明“原型
鏈?zhǔn)怯捎脩舸a來(lái)維護(hù)的,而不是編譯器維護(hù)的。
由這個(gè)推論,我們來(lái)看下面這個(gè)例子:
//---------------------------------------------------------
// 示例:錯(cuò)誤的原型鏈
//---------------------------------------------------------
// 1. 構(gòu)造器
function Animal() {}; // 動(dòng)物
function Mammal() {}; // 哺乳動(dòng)物
function Canine() {}; // 犬科的哺乳動(dòng)物
// 2. 構(gòu)造原型鏈
var instance = new Mammal();
Mammal.prototype = new Animal();
Canine.prototype = instance;
// 3. 測(cè)試輸出
var obj = new Canine();
document.writeln(obj instanceof Animal);
這個(gè)輸出結(jié)果,使我們看到一個(gè)錯(cuò)誤的原型鏈導(dǎo)致的結(jié)果“犬科的哺乳動(dòng)
物‘不是’一種動(dòng)物”。
根源在于“2. 構(gòu)造原型鏈”下面的幾行代碼是解釋執(zhí)行的,而不是象var和
function那樣是“聲明”并在編譯期被理解的。解決問(wèn)題的方法是修改那三
行代碼,使得它的“執(zhí)行過(guò)程”符合邏輯:
//---------------------------------------------------------
// 上例的修正代碼(部分)
//---------------------------------------------------------
// 2. 構(gòu)造原型鏈
Mammal.prototype = new Animal();
var instance = new Mammal();
Canine.prototype = instance;
?3). 原型實(shí)例是如何被構(gòu)造過(guò)程使用的
?------
?仍以Delphi為例。構(gòu)造過(guò)程中,delphi中會(huì)首先創(chuàng)建一個(gè)指定實(shí)例大小的
“空的對(duì)象”,然后逐一給屬性賦值,以及調(diào)用構(gòu)造過(guò)程中的方法、觸發(fā)事
件等。這個(gè)過(guò)程跟JavaScript中的行為是一致的:
//---------------------------------------------------------
// JS中的構(gòu)造過(guò)程(形式代碼)
//---------------------------------------------------------
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è)類為參考對(duì)象的,這個(gè)構(gòu)造過(guò)程中JavaScript可以擁有與Delphi
一樣豐富的行為。然而,由于Delphi中的構(gòu)造過(guò)程是“動(dòng)態(tài)的”,因此事實(shí)上
Delphi還會(huì)調(diào)用父類(MyObject)的構(gòu)造過(guò)程,以及觸發(fā)父類的OnCreate()事件。
JavaScript沒(méi)有這樣的特性。父類的構(gòu)造過(guò)程僅僅發(fā)生在為原型(prototype
屬性)賦值的那一行代碼上。其后,無(wú)論有多少個(gè)new MyObject2()發(fā)生,
MyObject()這個(gè)構(gòu)造器都不會(huì)被使用。——這也意味著:
? - 構(gòu)造過(guò)程中,原型模板是一次性生成的;對(duì)這個(gè)原型實(shí)例的使用是不斷復(fù)
??? 制,而并不再調(diào)用原型的構(gòu)造器。
由于不再調(diào)用父類的構(gòu)造器,因此Delphi中的一些特性無(wú)法在JavaScript中實(shí)現(xiàn)。
這主要影響到構(gòu)造階段的一些事件和行為?!獰o(wú)法把一些“對(duì)象構(gòu)造過(guò)程中”
的代碼寫(xiě)到父類的構(gòu)造器中。因?yàn)闊o(wú)論子類構(gòu)造多少次,這次對(duì)象的構(gòu)造過(guò)程根
本不會(huì)激活父類構(gòu)造器中的代碼。
所以再一次請(qǐng)大家看清楚new()關(guān)鍵字的形式代碼中的這一行:
//---------------------------------------------------------
// new()關(guān)鍵字的形式化代碼
//---------------------------------------------------------
function new(aFunction) { // 如果有參數(shù)args
? var _this = aFunction.prototype.clone(); // 從prototype中復(fù)制一個(gè)對(duì)象
? // ...
}
這個(gè)過(guò)程中,JavaScript做的是“prototype.clone()”,而Delphi等其它語(yǔ)言做
的是“Inherited Create()”。