Javascript繼承機制的設計思想

阮一峰發表於2011-06-05

我一直很難理解Javascript語言的繼承機制。

它沒有"子類"和"父類"的概念,也沒有"類"(class)和"例項"(instance)的區分,全靠一種很奇特的"原型鏈"(prototype chain)模式,來實現繼承。

我花了很多時間,學習這個部分,還做了很多筆記。但是都屬於強行記憶,無法從根本上理解。

Javascript繼承機制的設計思想

直到昨天,我讀到法國程式設計師Vjeux的解釋,才恍然大悟,完全明白了Javascript為什麼這樣設計。

下面,我嘗試用自己的語言,來解釋它的設計思想。徹底說明白prototype物件到底是怎麼回事。其實根本就沒那麼複雜,真相非常簡單。

一、從古代說起

要理解Javascript的設計思想,必須從它的誕生說起。

1994年,網景公司(Netscape)釋出了Navigator瀏覽器0.9版。這是歷史上第一個比較成熟的網路瀏覽器,轟動一時。但是,這個版本的瀏覽器只能用來瀏覽,不具備與訪問者互動的能力。比如,如果網頁上有一欄"使用者名稱"要求填寫,瀏覽器就無法判斷訪問者是否真的填寫了,只有讓伺服器端判斷。如果沒有填寫,伺服器端就返回錯誤,要求使用者重新填寫,這太浪費時間和伺服器資源了。

Javascript繼承機制的設計思想

因此,網景公司急需一種網頁尾本語言,使得瀏覽器可以與網頁互動。工程師Brendan Eich負責開發這種新語言。他覺得,沒必要設計得很複雜,這種語言只要能夠完成一些簡單操作就夠了,比如判斷使用者有沒有填寫表單。

Javascript繼承機制的設計思想

1994年正是物件導向程式設計(object-oriented programming)最興盛的時期,C++是當時最流行的語言,而Java語言的1.0版即將於第二年推出,Sun公司正在大肆造勢。

Brendan Eich無疑受到了影響,Javascript裡面所有的資料型別都是物件(object),這一點與Java非常相似。但是,他隨即就遇到了一個難題,到底要不要設計"繼承"機制呢?

二、Brendan Eich的選擇

如果真的是一種簡易的指令碼語言,其實不需要有"繼承"機制。但是,Javascript裡面都是物件,必須有一種機制,將所有物件聯絡起來。所以,Brendan Eich最後還是設計了"繼承"。

但是,他不打算引入"類"(class)的概念,因為一旦有了"類",Javascript就是一種完整的物件導向程式語言了,這好像有點太正式了,而且增加了初學者的入門難度。

他考慮到,C++和Java語言都使用new命令,生成例項。

C++的寫法是:

  ClassName *object = new ClassName(param);

Java的寫法是:

  Foo foo = new Foo();

因此,他就把new命令引入了Javascript,用來從原型物件生成一個例項物件。但是,Javascript沒有"類",怎麼來表示原型物件呢?

這時,他想到C++和Java使用new命令時,都會呼叫"類"的建構函式(constructor)。他就做了一個簡化的設計,在Javascript語言中,new命令後面跟的不是類,而是建構函式。

舉例來說,現在有一個叫做DOG的建構函式,表示狗物件的原型。

  function DOG(name){

    this.name = name;

  }

對這個建構函式使用new,就會生成一個狗物件的例項。

  var dogA = new DOG('大毛');

  alert(dogA.name); // 大毛

注意建構函式中的this關鍵字,它就代表了新建立的例項物件。

三、new運算子的缺點

用建構函式生成例項物件,有一個缺點,那就是無法共享屬性和方法。

比如,在DOG物件的建構函式中,設定一個例項物件的共有屬性species。

  function DOG(name){

    this.name = name;

    this.species = '犬科';

  }

然後,生成兩個例項物件:

  var dogA = new DOG('大毛');

  var dogB = new DOG('二毛');

這兩個物件的species屬性是獨立的,修改其中一個,不會影響到另一個。

  dogA.species = '貓科';

  alert(dogB.species); // 顯示"犬科",不受dogA的影響

每一個例項物件,都有自己的屬性和方法的副本。這不僅無法做到資料共享,也是極大的資源浪費。

四、prototype屬性的引入

考慮到這一點,Brendan Eich決定為建構函式設定一個prototype屬性。

這個屬性包含一個物件(以下簡稱"prototype物件"),所有例項物件需要共享的屬性和方法,都放在這個物件裡面;那些不需要共享的屬性和方法,就放在建構函式里面。

例項物件一旦建立,將自動引用prototype物件的屬性和方法。也就是說,例項物件的屬性和方法,分成兩種,一種是本地的,另一種是引用的。

還是以DOG建構函式為例,現在用prototype屬性進行改寫:

  function DOG(name){

    this.name = name;

  }

  DOG.prototype = { species : '犬科' };


  var dogA = new DOG('大毛');

  var dogB = new DOG('二毛');


  alert(dogA.species); // 犬科

  alert(dogB.species); // 犬科

現在,species屬性放在prototype物件裡,是兩個例項物件共享的。只要修改了prototype物件,就會同時影響到兩個例項物件。

  DOG.prototype.species = '貓科';


  alert(dogA.species); // 貓科

  alert(dogB.species); // 貓科

五、總結

由於所有的例項物件共享同一個prototype物件,那麼從外界看起來,prototype物件就好像是例項物件的原型,而例項物件則好像"繼承"了prototype物件一樣。

這就是Javascript繼承機制的設計思想。不知道我說清楚了沒有,繼承機制的具體應用方法,可以參考我寫的系列文章:

  * 《Javascript物件導向程式設計(一):封裝》

  * 《Javascript物件導向程式設計(二):建構函式的繼承》

  * 《Javascript物件導向程式設計(三):非建構函式的繼承》

(完)

相關文章