javascript 原型繼承(第四篇)---幾種繼承方式
今天要介紹的是,物件之間的”繼承”的幾種方式。
首先有一個問題,為什麼繼承還有幾種方式呢?你看不管是java還是C++,繼承就是繼承,哪有幾種繼承方式,不過Javascript 是一種靈活的語言,之所以靈活,說不好聽點就是設計得太簡單,連基本的繼承都要自己去實現,下面就說一下最常用的幾種繼承方式。
比如,現在有一個”動物”物件的建構函式。
function Animal(){
this.species = "動物";
}
還有一個”貓”物件的建構函式。
function Cat(name,color){
this.name = name;
this .color = color;
}
怎樣才能使”貓”繼承”動物”呢?
一、建構函式繼承
第一種方法也是最簡單的方法,使用call或apply方法,將父物件的建構函式繫結在子物件上,即在子物件建構函式中加一行:
function Cat(name,color){
Animal.apply(this, arguments);//在子物件增加這一行程式碼
this.name = name;
this.color = color;
}
var cat1 = new Cat("大毛","黃色");
alert(cat1.species); // 動物
二、原型鏈繼承
第二種方法更常見,使用prototype屬性,也就是常說的原型鏈繼承。
如果”貓”的prototype物件,指向一個Animal的例項,那麼所有”貓”的例項,就能繼承Animal了。
Cat.prototype = new Animal();
Cat.prototype.constructor = Cat;
var cat1 = new Cat("大毛","黃色");
alert(cat1.species); // 動物
程式碼的第一行,我們將Cat的prototype物件指向一個Animal的例項。
Cat.prototype = new Animal();
它相當於完全刪除了prototype 物件原先指向的值,然後指向了一個新值。但是,第二行又是什麼意思呢?
Cat.prototype.constructor = Cat;//這一行什麼意思呢?
原來,任何一個建構函式都有一個prototype屬性,prototype的屬性值指向一個物件,(prototype相當於一個指標,我們把它指向的物件暫叫prototype物件),而且這個prototype物件預設有一個constructor屬性,指向這個建構函式本身。如下圖所示,
SuperType是一個建構函式,右側的方框就是它的原型,SuperType的prototype屬性指向SuperType Prototype物件,SuperType Prototype物件的constructor屬性指向它的建構函式。
如果沒有”Cat.prototype = new Animal();”這一行,Cat.prototype.constructor是指向Cat的;加了這一行以後,Cat.prototype.constructor指向Animal。
Cat.prototype = new Animal();
alert(Cat.prototype.constructor == Animal); //true
更重要的是,每一個例項也有一個constructor屬性,預設呼叫prototype物件的constructor屬性。
alert(cat1.constructor == Cat.prototype.constructor); // true
因此,在執行”Cat.prototype = new Animal();”這一行之後,cat1.constructor也指向了Animal。
alert(cat1.constructor == Animal); // true
這顯然會導致繼承鏈的紊亂(cat1明明是用建構函式Cat生成的,但是cat1.constructor卻指向了Animal), 因此我們必須手動糾正,將Cat.prototype物件的constructor值改為Cat。 這就是第二行的意思。
這是很重要的一點,程式設計時務必要遵守, 在用原型鏈繼承時,都要記著,如果替換了prototype物件
A.prototype = B;
那麼,下一步必然是為prototype物件的constructor屬性指回原來的建構函式。
A.prototype.constructor = A;
三、原型鏈繼承的改進
第三種方法是對第二種方法的改進。由於Animal物件中,不變的屬性都可以直接寫入Animal.prototype。所以,我們也可以讓Cat()跳過 Animal(),直接繼承Animal.prototype。
現在,我們先將Animal物件改寫:
function Animal(){ }
Animal.prototype.species = "動物";
然後,將Cat的prototype物件,然後指向Animal的prototype物件,這樣就完成了繼承。
Cat.prototype = Animal.prototype;
Cat.prototype.constructor = Cat;
var cat1 = new Cat("大毛","黃色");
alert(cat1.species); // 動物
與前一種方法相比,這樣做的優點是效率比較高(不用執行和建立Animal的例項了),比較省記憶體。缺點是 Cat.prototype和Animal.prototype現在指向了同一個物件,那麼任何對Cat.prototype的修改,都會反映到Animal.prototype。
所以,上面這一段程式碼其實是有問題的。請看第二行;
Cat.prototype.constructor = Cat;
這一句實際上把Animal.prototype物件的constructor屬性也改掉了!
alert(Animal.prototype.constructor); // Cat
到這裡,大家可以和這篇部落格的第二部分的作一個對比,在第二部分中,實現繼承的關鍵程式碼是
Cat.prototype = new Animal();
Cat.prototype.constructor = Cat;
可以驗證的是,
alert(Animal.prototype.constructor)//Animal
這就是
Cat.prototype = new Animal();//prototype相當於一個指標,指向一個物件,這裡直接把new Animal()這個例項直接賦值給prototype指向的那個物件。
和
Cat.prototype = Animal.prototype;//這裡便形成了指標的重定向,Cat的prototype指向了Animal的prototype所指向的那個物件,
之間的區別!所以這也就是Animal.prototype.constructor的指向一個沒改變,一個改變了。
四、組合繼承(最常用)
給Animal增加一個屬性—-函式
function Animal(){
this.species = "動物";
}
Animal.prototype.fun = function(){};//給Animal增加一個函式
實現Cat的繼承,
function Cat(){
Super.call(this); // 核心
this.name = name;
this.color = color
}
Cat.prototype = new Animal(); // 核心
var cat1 = new Cat("大毛","黃色");
alert(cat1.species); // 動物
alert(sub1.fun === sub2.fun); // true,說明sub1.fun和sub2.fun指向相同的地址,這便是組合繼承的優點,各種繼承方式的優缺點將在下一篇講
到此,我們常用的javascript繼承方式已經講完了,相信也有人說還有拷貝繼承的方式,但個人不太喜歡拷貝繼承,即使是做js專案,以上的繼承方式便足夠了,我相信在實際程式設計中,你也只會用一種繼承方式。
參考自阮一峰的部落格(另提一下,在阮一峰老師的這篇部落格裡的第四部分是有問題的,個人親測,第五部分好像也有問題,也不太常用,就不放在我的部落格裡了)