================================================================================
Qomolangma OpenProject v0.9
類別 :Rich Web Client
關鍵詞 :JS OOP,JS Framwork, Rich Web Client,RIA,Web Component,
DOM,DTHML,CSS,JavaScript,JScript
項目發起:aimingoo (aim@263.net)
項目團隊:aimingoo, leon(pfzhou@gmail.com)
有貢獻者:JingYu(zjy@cnpack.org)
================================================================================
八、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)的一個引用(在ECMA Javascript
規範中,對象的這個屬性名稱定義為__proto__)。
每一個函數,無論它是否用作構造器,都會有一個獨一無二的原型對象(prototype)。
對於JavaScript“內建對象的構造器”來說,它指向內部的一個原型。預設時JavaScript
構造出一個“空的初始對象執行個體(不是null)”並使原型引用指向它。然而如果你給函
數的這個prototype賦一個新的對象,那麼新的對象執行個體將執有它的一個引用。
接下來,構造過程將調用MyObject()來完成初始化。——注意,這裡只是“初始
化”。
為了清楚地解釋這個過程,我用代碼形式化地描述一下這個過程:
//---------------------------------------------------------
// new()關鍵字的形式化代碼
//---------------------------------------------------------
function new(aFunction) {
// 基本對象執行個體
var _this = {};
// 原型引用
var _proto= aFunction.prototype;
/* if compat ECMA Script
_this.__proto__ = _proto;
*/
// 為存取原型中的屬性添加(內部的)getter
_this._js_GetAttributes= function(name) {
if (_existAttribute.call(this, name))
return this[name]
else if (_js_LookupProperty.call(_proto, name))
retrun OBJ_GET_ATTRIBUTES.call(_proto, name)
else
return undefined;
}
// 為存取原型中的屬性添加(內部的)setter
_this._js_GetAttributes = function(name, value) {
if (_existAttribute.call(this, name))
this[name] = value
else if (OBJ_GET_ATTRIBUTES.call(_proto, name) !== value) {
this[name] = value // 建立當前執行個體的新成員
}
}
// 調用建構函式完成初始化, (如果有,)傳入args
aFunction.call(_this);
// 返回對象
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中的new()關鍵字中隱含的構造過程,與Delphi的構造過程並不完全一致。但
在構造器函數中發生的行為卻與上述的類似:
//---------------------------------------------------------
// 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中實現。
這主要影響到構造階段的一些事件和行為。——無法把一些“物件建構過程中”
的代碼寫到父類的構造器中。因為無論子類構造多少次,這次對象的構造過程根
本不會啟用父類構造器中的代碼。
JavaScript中屬性的存取是動態,因為對象存取父類屬性依賴於原型鏈表,構造
過程卻是靜態,並不訪問父類的構造器;而在Delphi等一些編譯型語言中,(不使
用讀寫器的)屬性的存取是靜態,而對象的構造過程則動態地調用父類的建構函式。
所以再一次請大家看清楚new()關鍵字的形式代碼中的這一行:
//---------------------------------------------------------
// new()關鍵字的形式化代碼
//---------------------------------------------------------
function new(aFunction) {
// 原型引用
var _proto= aFunction.prototype;
// ...
}
這個過程中,JavaScript做的是“get a prototype_Ref”,而Delphi等其它語言做
的是“Inherited Create()”。
(本節未完待續...)