ECMAScript只支持實現繼承,而且其實現繼承主要是依靠原型鏈來實現的。
原型鏈
原型鏈的基本思想是利用原型讓一個引用類型繼承另一個引用類型的屬性和方法。每一個構造函數都有一個原型對象,原型對象都包含一個指向構造函數的指針,而實例都包含一個指向原型對象的指針。如果:我們讓原型對象A等於另一個類型B的實例,那麼原型對象A就會有一個指針指向B的原型對象,相應的B的原型對象中保存著指向其構造函數的指針。假如B的原型對象又是另一個類型的實例,那麼上述的關系依舊成立,如此層層遞進,就構成了實例與原型的鏈條。
實例以及構造函數和原型之間的關系圖如下所示:
person.constructor現在指向的是Parent,這是因為Child.prototype指向了Parent的原型,而Parent原型對象的constructor指向Parent。
當以讀取模式訪問一個實例屬性時,首先會在實例中搜索該屬性,如果沒有找到該屬性,則會繼續搜索實例的原型。在通過原型鏈實現的集成中,搜索過程就會沿著原型鏈繼續向上,直到搜索到原型鏈的末端。
例如,調用person.getParentValue()方法,1)搜索實例;2)搜索Child.prototype;3)搜索Parent.prototype;找到了getParentValue()方法停止。
1、默認的原型
前面的例子中展示的原型鏈少了一環,所有引用類型默認都繼承了Object,而這個繼承也是通過原型鏈實現的。因此默認的原型都包含一個內部指針,指向Object.prototype,這也正是所有自定義類型會繼承toString()、ValueOf()等默認方法的根本原因。換句話說Object.prototype就是原型鏈的末端。
2、確定原型和實例的關系
通過兩種方式可以確定原型和實例之間的關系,第一種是使用instanceOf操作符,第二種是使用isPrototypeOf()方法。
實例 instanceOf 原型鏈 中出現過的構造函數,都會返回true
console.log(person instanceOf Child);//true console.log(person instanceOf Parent);//true console.log(person instanceOf Object);//true isPrototype(),只要是原型鏈中出現過的原型,都可以說是該原型鏈所派生出來的實例的原型,因此也返回true. console.log(Object.prototype.isPrototypeOf(instance));//true console.log(Parent.prototype.isPrototypeOf(instance));//true console.log(Child.prototype.isPrototypeOf(instance));//true
3、謹慎地定義方法
子類型有時候需要覆蓋超類型中的某個方法,或者需要添加超類型中不存在的莫個方法,注意:給原型添加方法的代碼一定要放在替換原型的語句之後。
當通過Child的實例調用getParentValue()時,調用的是這個重新定義過的方法,但是通過Parent的實例調用getParentValue()時,調用的還是原來的方法。
格外需要注意的是:必須要在Parent的實例替換原型之後,再定義這兩個方法。
還有一點需要特別注意的是:通過原型鏈實現繼承時,不能使用對象字面量創建原型方法,因為這樣做會重寫原型鏈。
以上代碼剛把Parent的實例賦值給Child的原型對象,緊接著又將原型替換成一個字面量,替換成字面量之後,Child原型實際上包含的是一個Object的實例,而不再是Parent的實例,因此我們設想中的原型鏈被切斷.Parent和Child之間沒有任何關聯。
4、原型鏈的問題
原型鏈很強大,可以利用它來實現繼承,但是也有一些問題,主要的問題還是包含引用類型值的原型屬性會被所有實例共享。因此我們在構造函數中定義實例屬性。但是在通過原型來實現繼承時,原型對象其實變成了另一個類型的實例。於是原先定義在構造函數中的實例屬性變成了原型屬性了。
舉例說明如下:
在Parent構造函數中定義了一個friends屬性,該屬性值是一個數組(引用類型值)。這樣,Parent的每個實例都會各自包含自己的friends屬性。當Child通過原型鏈繼承了Parent之後,Child.prototype也用用了friends屬性——這就好像friends屬性是定義在Child.prototype一樣。這樣Child的所有實例都會共享這個friends屬性,因此我們對kid1.friends做的修改,在kid2.friends中也會體現出來,顯然,這不是我們想要的。
原型鏈的另一個問題是:在創建子類型的實例時,不能在不影響所有對象實例的情況下,給超類型的構造函數傳遞參數。因此,我們通常很少會單獨使用原型鏈。
借用構造函數
為了解決原型中包含引用類型值帶來的一些問題,引入了借用構造函數的技術。這種技術的基礎思想是:在子類型構造函數的內部調用超類型構造函數。
Parent.call(this)在新創建的Child實例的環境下調用了Parent構造函數。在新創建的Child實例環境下調用Parent構造函數。這樣,就在新的Child對象上,此處的kid1和kid2對象上執行Parent()函數中定義的對象初始化代碼。這樣,每個Child實例就都會具有自己的friends屬性的副本了。
借用構造函數的方式可以在子類型的構造函數中向超類型構造函數傳遞參數。
為了確保子類型的熟悉不會被父類的構造函數重寫,可以在調用父類構造函數之後,再添加子類型的屬性。
構造函數的問題:
構造函數模式的問題,在於方法都在構造函數中定義,函數復用無從談起,因此,借用構造函數的模式也很少單獨使用。
組合繼承
組合繼承指的是將原型鏈和借用構造函數的技術組合在一塊,從而發揮二者之長。即:使用原型鏈實現對原型屬性和方法的繼承,而通過借用構造函數來實現對實例屬性的繼承。
Person構造函數定義了兩個屬性:name和friends。Person的原型定義了一個方法sayName()。Child構造函數在調用Parent構造函數時,傳入了name參數,緊接著又定義了自己的屬性age。然後將Person的實例賦值給Child的原型,然後又在該原型上定義了方法sayAge().這樣,兩個不同的Child實例既分別擁有自己的屬性,包括引用類型的屬性,又可以使用相同的方法了。
組合繼承避免了原型鏈和構造函數的缺陷,融合了他們的有點,成為JavaScript中最常用的繼承模式。而且,instanceOf和isPropertyOf()也能夠識別基於組合繼承創建的對象。
最後,關於JS對象和繼承都還有幾種模式沒有寫,或者說,我自己也還未去深刻研究,但是,我想,首先將組合模式應用的游刃有余。並且,對於為何選用組合模式,知其然,知其所以然。
關於JavaScript實現繼承的幾種方式(推薦),小編就給大家介紹到這裡,希望對大家有所幫助!