MDN’s new design is in Beta! A sneak peek: https://blog.mozilla.org/opendesign/mdns-new-design-beta/

 

繼承

這裡有很多有關如何在 JavaScript 中定義類型(類別)的困惑之處,包括繼承。這裡有很多方式和一些 次優的選擇。這裡所展示的運作的最好,而且簡單。

範例

B 繼承 A︰

function A(a)
{
  this.varA = a;
}
A.prototype =
{
  varA : null,
  doSomething : function()
  {
     ...
  }
}
function B(a, b)
{
  A.call(this, a);
  this.varB = b;
}
B.prototype =
{
  varB : null,
  doSomething : function() // 覆寫
  {
     A.prototype.doSomething.apply(this, arguments); // 呼叫親類型
     ...
  }
}
extend(B, A);

var b = new B();
b.doSomething();

重點部分是︰

  • 類型是在 .prototype 裡定義的
  • 使用 extend() 來繼承

此處的 extend() 並不是內建的函數,其定義如下︰

function extend(child, supertype)
{
   child.prototype.__proto__ = supertype.prototype;
}

prototype 和 __proto__

JavaScript 對於來自 Java 或 C++ 的開發者而言會感到一些困惑,因為他完全動態,完全執行時期,而且完全沒有類別。他完全只有實體(物件)。甚至〝類別〞也只是函數物件模擬出來的。

你或許已經注意到前面我們的 function A 很特別,這並不是普通的函數,但可以和關鍵字 new 結合使用來實體化新的物件。他也可以有稱作 prototype 的特別的屬性(由 JS 標準所定義)。這個函數所做的不過是,當你呼叫 new 的時候,參考的 prototype 物件被複製到新的實體的屬性 __proto__。也就是當你這樣做 var a1 = new A() 的時候,JS(在記憶體中建立物件之後,並在使用 this 執行函數 A() 來定義他之前)只是簡單的這樣做 a1.__proto__ = A.prototype。然後當你存取實體的屬性的時候,JS 首先會檢查那些是否直接存在於物件上,如果不是的話,就從 __proto__ 搜尋。這意思是所有你定義在 prototype 裡的東西,實際上會被所有的實體所共用,而且你甚至可以在稍後修改 prototype 的部分,並且在所有既存的實體上表現出這個改變,只要你想要的話。

舉例來說,當你在上面的範例中這樣做 var a1 = new A(); var a2 = new A();,然後 a1.doSomething 實際上會參考 a1.__proto__.doSomething,這些和你定義的 A.prototype.doSomething 相同,也就是 a1.__proto__.doSomething == a2.__proto__.doSomething == A.prototype.doSomething

簡而言之prototype 是對類型而言,而 __proto__ 對實體而言都相同。

__proto__ 是以遞歸的方式來看待的,也就是 a1.doSomethinga1.__proto__.doSomethinga1.__proto__.__proto__.doSomething 等等,直到找到或不存在 __proto__ 為止。

所以,發生了什麼︰當你呼叫
  var o = new Foo()
JS 實際上只是這樣做
  var o = new Object();
  o.__proto__ = Foo.prototype;
  o.Foo();
(諸如此類)

以及當你隨後這樣做
  o.someProp
他會檢查 o 是否有屬性 someProp,如果沒有就檢查 o.__proto__.someProp,如果沒有就檢查 o.__proto__.__proto__.someProp 依此類推。這個最後的步驟就是 extend() 函數運作的原因。

 

注意,__proto__ 只能在 Mozilla 的 JS 引撉中存取。其他引撉的也有相同的運作,但不能存取 __proto__。參閱以下內容來補救。

extend() 的另一個選擇

還有另一個選擇,你也可以定義 extend() 如下︰

function extend(child, supertype)
{
   child.prototype.__proto__ = supertype.prototype;
   child.prototype.__super = supertype;
}

因此,當你想要在 B 中呼叫親函數的時候,你可以使用 this.__super 取代 A,例如 this.__super.call(this, a) 用於建構子,以及 this.__super.prototype.doSomething.apply(this, arguments) 用於覆載函數。

注意,__proto__ 在 Mozilla 以外的 JavaScript 版本中可能無法使用。還有另一個選擇,但不是 extend() 的最佳版本,這次應該到處都可以用︰

function extend(child, super)
{
  for (var property in super.prototype) {
    if (typeof child.prototype[property] == "undefined")
      child.prototype[property] = super.prototype[property];
  }
  return child;
}

這次簡單的直接把親類型的 prototype 裡所有的屬性和函數,放入到子類型的 prototype 裡。這對多重繼承而言非常有用,但要小心使用,沒有親類別會把屬性或函數定義成同樣的,或者你需要明確的把那些覆載並定義那些該怎麼做。

阻礙

這裡有另一個方法用來定義類型,例如︰

function A()
{
  this.varA = "bla";
  this.isNotSoGood = function()
  {
    ...
  };
}

這樣也可以運作,但只是次優的選擇,因為每次你建立這個類型的物件的時候,isNotSoGood 會在每一個實體中定義一次。如果是在 .prototype 定義這些的話,就只會定義一次。


你可以使用下面的方式來繼承︰

B.prototype = new A();

不在載入 JS 檔案的時候建立 A() 的實體。這是非常壞的點子,因為你可能需要在 A 的建構子裡做一些處理,這可能會耗費時間並因此大幅延遲載入,或嘗試存取尚未載入的東西(例如,在 utils.js 裡的函數)。

文件標籤與貢獻者

 此頁面的貢獻者: teoli, happysadman
 最近更新: teoli,