JavaScript物件導向的支援(4)

來源:互聯網
上載者:User

================================================================================
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()”。

(本節未完待續...)

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.