今天要介紹的是,對象之間的”繼承”的幾種方式。
首先有一個問題,爲什麼繼承還有幾種方式呢?你看不管是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項目,以上的繼承方式便足夠了,我相信在實際編程中,你也只會用一種繼承方式。
第五篇: javascript原型繼承(第五篇)—幾種繼承方式的優缺點
參考自阮一峯的博客(另提一下,在阮一峯老師的這篇博客裏的第四部分是有問題的,個人親測,第五部分好像也有問題,也不太常用,就不放在我的博客裏了)