最近一直在研究js面向對象,原型鏈繼承是一個難點,下面是我對繼承的理解
如下文章借鑑自CSDN季詩筱的博客數組
ES中描述了原型鏈的概念,並將原型鏈做爲實現繼承的主要方法;
基本思想:利用一個引用類型繼承另外一個引用類型的屬性和方法:
簡單回顧下: 構造函數 -- 原型 -- 實例 三者之間的關係
構造函數:function Person(){}
每一個構造函數都有一個原型對象(Person.prototype),
原型對象都包含一個指向構造函數的指針(constructor),
(其實原型對象也是一個對象,也有一個 __proto__
指針,指向他所繼承的對象)
而實例都包含着一個指向圓形對象的內部指針([[prototye]] 又稱__proto__);
每一個實例也有一個constructor屬性默認調用原型對象的constructor屬性(!!!)函數
讓原型對象等於另外一個構造函數的實例, 顯然,此時的原型對象將包含一個指向另外一個原型對象的指針([[prototype]]),從而擁有該原型對象的屬性和方法this
另外一個原型對象中也包含着指向另外一個構造函數的指針。那麼上述關係依然成立,如此層層遞進,就構成了實例與原型的鏈條。這就是所謂原型鏈的基本概念!spa
如上如所示,這種關係直到 當某個原型對象的 contructor屬性指向 Object 爲止.net
function SuperType(){ this.prototype = true; } SuperType.prototype.getSuperValue = function(){ return this.property; } function SubType(){ this.subproperty = false; } //繼承了SuperType SubType.prototype = new SuperType(); SubType.prototype.getSubValue = function(){ return this.subproperty; } var instance = new SubType(); alert(instance.getSuperValue()); //true
以上代碼定義了兩個類型:SuperType 和 SubType.
每一個類型分別有一個屬性和方法。
他們的主要區別是SubType繼承了SuperType,而繼承是經過建立SuperType的實例,並將該實例賦值給SubType.prototype實現的。
實現的本質是重寫原型對象,代之以一個新類型的實例。
換句話說,原來存在於SuberType的實例中的全部屬性和方法,如今存在於SubType.prototype中了。
在確立了繼承關係以後,咱們給SubType.prototype添加了一個方法,,這樣就在繼承了SuperType的屬性和方法的基礎上又添加了一個新方法。
這個例子的實例以及構造函數和原型之間的關係以下圖所示
圖:prototype
在上面的代碼中,咱們沒有使用SubType默認提供的原型,而是給他換了一個新原型;
這個原型就是SuperType的實例。
因而,新原型不只有做爲一個SuperType的實例所擁有的所有屬性和方法,並且內部還有一個指針,指向了SuperType的原型。
最終結果是這樣的:instace指向SubType的原型,SubType的原型又指向SuperType的原型。指針
在經過原型鏈繼承的狀況下,搜索過程就得以沿着原型鏈繼續向上。就拿上面的例子來講,調用instance.getSuperValue()會經歷三個步驟:
1.搜索實例
2.搜索SubType.prototype
3.搜索SuperType.prototype
最後一步纔會找到該方法,再找不到該屬性或方法的狀況下,搜索過程老是要一環一環地前行到原型鏈末端纔會停下來.
別忘記默認原型Object
事實上前面例子中展現的原型鏈還少一環,咱們知道,全部引用類型都默認繼承了Object,而這個繼承也是經過原型鏈實現的。
你們記住,全部函數的默認原型都是Object的實例,所以默認原型內部都會包含一個指針,指向Object.prototype。這也正是全部自定義類型都會繼承toString()等默認方法的根本緣由.code
原型鏈雖然很強大,能夠用它實現繼承,但也存在一些問題。對象
其中,最主要的問題來自包含引用類型值的原型。
想必你們還記得,咱們前面介紹過包含引用類型值的屬性會被全部實例共享;而這也正是爲何要在構造函數中,而不是在原型對象中定義屬性的緣由。blog
在經過原型來實現繼承時,原型實際上會變成另外一個類型的實例。因而原先的實例屬性也就瓜熟蒂落地變成了如今的原型屬性了.
例子說明問題:
function SuperType(){ this.colors = ["red","blue","green"]; } function SubType(){ } SubType.prototype = new SuperType(); // 繼承了SuperType var instance1 = new SubType(); instance1.colors.push("black"); alert(instance1.colors); // red,blue,green,black var instance2 = new SubType(); alert(instance2.colors); // red,blue,green,black
這個例子中的SuperType構造函數定義了一個colors屬性,該屬性包括一個數組(引用類型值)。SuperType的每一個實例都會有各自包含本身數組的colors屬性。當SubType經過原型鏈繼承了SuperType以後,SubType.prototype就變成了SuperType的一個實例,所以他也擁有了一個他本身的colors屬性-----就跟專門建立了一個SubType.prototype.colors屬性同樣。
但結果是什麼呢? 全部實例都會共享這個colors屬性!!!
問題1:結果是SubType的全部實例都會共享這一個colors屬性。而咱們對instance1.colors的修改可以經過instance2.colors反映出來,就已經充分證實這一點了
問題2:在建立自定義類型的時候,不能向超類型的構造函數中傳遞參數。
實際上,應該說是沒有辦法在不影響全部實例的狀況下,給超類型構造函數傳遞參數。
有鑑於此,在加上前面剛剛討論的因爲原型中所包含引用類型值所帶來的問題,實踐中中不多單獨使用原型鏈
1.借用構造函數2.組合式繼承
3.原型式繼承
4.寄生式繼承5.寄生組合式繼承
這裏來談一下最經常使用的組合式繼承和寄生組合式繼承
組合繼承,有時候也叫作經典繼承,指的是將原型鏈和借用構造函數的技術組合到一塊兒,從而發揮兩者之長的一種繼承模式。
其背後的思路是使用原型鏈實現對原型屬性和方法的繼承,而經過借用構造函數來實現對實例的屬性的繼承
請看例子:
function SuperType(name){ this.name = name; this.colors = ["red","blue","green"]; } SuperType.prototype.sayName = function(){ alert(this.name); } function SubType(name,age){ SuperType.call(this,name); //繼承屬性, 第二次調用SuperType this.age = age; } //繼承方法 SubType.prototype = new SuperType(); //第一次調用SuperType SubType.prototype.constructor = SubType(); // 至關重要,此處 SubType.prototype.sayAge = function(){ alert(this.age); } var instance1 = new SubType("leo",29); instance1.colors.push("black"); alert(instance1.colors); // r,b,g,b instance1.sayName(); // leo instance1.sayAge(); // 29 var instance2 = new SubType("lck",34); alert(instance2.colors); // r,b,g instance2.sayName(); // lck instance2.sayAge(); // 34
在例子中,SuperType構造函數定義了兩個屬性:name 和 colors。SuperType的原型定義了一個方法sayName()。
SubType構造函數在調用SuperType構造函數傳入了name參數,緊接着又定義了他本身的屬性age,而後,將SuperType的實例賦值給SubType的原型,而後又在該新原型上定義了方法sayAge()方法,
這樣一來,就可讓兩個不一樣的SubType實例既分別擁有本身屬性和公共的colors屬性,又可使用相同的方法
組合繼承避免了原型鏈和借用構造函數的缺陷,融合了他們的優勢,成爲js中最經常使用的繼承模式。
並且 instanceof和isPrototypeOf()也可以用於識別基於組合繼承建立的對象
**另外說一下:
1.任何一個Prototype對象都有一個constructor指針,指向它的構造函數;
2.每一個實例中也會有一個constructor指針,這個指針默認調用Prototype對象的constructor屬性。
結果:當替換了子類的原型以後,即 SubType.prototype = new SuperType()以後,
SubType.prototype.constructor 就指向了SuperType(),
SubType的實例的constructor也指向了SuperType(),這就出現問題了。
由於這形成了繼承鏈的紊亂,由於SubType的實例是由SubType構造函數建立的,如今其constructor屬性卻指向了SuperType,爲了不這一現象,就必須在替換prototype對象以後,爲新的prototype對象加上constructor屬性,使其指向原來的構造函數。
組合式繼承的缺點
組合繼承最大的問題就是不管在什麼狀況下,都會兩次調用超類型構造函數;
一次是在建立子類型的原型的時候,
另外一次是在子類型構造函數內部。
沒錯子類型最終會包含超類型對象的所有實例屬性,但咱們不得不在調用子類構造函數時重寫這些屬性
此種模式解決了組合式繼承的缺點
原理:經過借用構造函數來繼承屬性,經過原型鏈的混成形式來繼承方法。
思路:沒必要爲了指定子類的原型而調用超類型的構造函數,咱們所須要的無非就是超類型原型的一個副本而已。
寄生式組合繼承的基本模式以下所示:
function inheritPrototype(subType,superType){ var prototype = object(superType.prototype); // 建立對象 prototype.constructor = subType; // 加強對象 subType.prototype = prototype; // 指定對象 }
這個示例中的inheritPrototype()函數實現了寄生式組合繼承的最簡單形式。
這個函數接收兩個參數:子類型構造函數和超類型構造函數。
在函數內部:
第一步:是建立超類型原型的一個副本
第二步:爲建立的副本添加constructor屬性,從而彌補因失去原型而失去的默認的constructor屬性
第三步:將建立的對象(即副本)賦值給子類型的原型。
這樣,咱們就能夠調用inherit-Protoype()函數的語句,去替換前面例中爲了子類型原型賦值的語句了
實例以下:
//藉助原型能夠基於已有對象建立新對象 function object(o){ var F = function(){}; // 建立一個空對象 F.prototype = o; return new F(); //返回出一個實例對象 } //寄生式組合繼承 function inheritPrototype(subType, superType){ var prototype = object(superType.prototype); //建立父構造函數實例對象副本 prototype.constructor = subType; // 重寫將子類原型對象的constructor屬性 subType.prototype = prototype; // 父類實例賦值給子類原型 }; function SuperType(name){ this.name = name; this.colors = ["red", "blue", "green"]; } SuperType.prototype.sayName = function(){ alert(this.name); }; function SubType(name, age){ SuperType.call(this, name); this.age = age; } //調用此方法代替前面賦值語句,可解決兩次調用超類型構造函數的問題 inheritPrototype(SubType, SuperType); SubType.prototype.sayAge = function(){ alert(this.age); }; var instance1 = new SubType("lck",29); console.log(instance1.name,instance1.age,instance1.colors); // lck,29,r,b,g instance1.sayName(); // lck instance1.sayAge() // 29
優勢:
這個例子的高效率體如今他只調用了一次SuperType構造函數,
而且所以避免了在SubType.prototype上面建立沒必要要的,多餘的屬性。
與此同時,原型鏈還能保持不變;
所以,還能正常使用instanceof 和 isPrototypeOf()。
開發人員廣泛認爲寄生式組合式繼承是引用類型最理想的繼承範式