繼承是面相對象編程語言的一個特點,通常分爲兩類:接口繼承和實現繼承。接口繼承只繼承方法簽名,而實現繼承則繼承實際的方法。在 JS 中,沒有函數簽名,所以,JS 只支持實現繼承,主要是經過原型鏈實現的。編程
先來回顧下構造函數,原型對象,實例,三者之間的關係:segmentfault
每建立一個函數,就會爲函數建立一個 prototype
屬性,指向原型對象;app
原型對象,默認狀況下,會自動得到一個 constructor
屬性,指回構造函數;編程語言
經過構造函數建立的實例,會有一個內部屬性 [[prototype]]
,指向原型對象。函數
關係以下圖:this
讓子類的原型對象 等於 父類的實例,Child.prototype = new Parent()
;es5
function Parent() { this.parentAge = 56; } Parent.prototype.getParentAge = function() { return this.parentAge; }; function Child() { this.childAge = 18; } Child.prototype = new Parent(); Child.prototype.getChildAge = function () { return this.childAge; }; var xiaoming = new Child(); console.log(xiaoming.getParentAge()); // 56
關係圖以下:spa
注意 上圖中 Child()
的原型對象 new Parent()
的 constructor
並不指向 Child()
(此時它指向 Function)。後續咱們會手動讓它指向 Child()
。prototype
事實上,上圖少了一環,默認的原型。咱們知道全部的引用類型都默認繼承了 Object,而這個繼承也是經過原型鏈實現的。全部函數的默認原型都是 Object 的實例,所以,默認原型都會包含一個內部指針,指向 Object.prototype。這也是全部自定義類型都會繼承 toString(),valueOf等方法的緣由所在。指針
從上圖中,能夠清晰的看到原型鏈逐級向上查找的路徑。
xiaoming
同時是 Child
,Parent
,Object
的實例;
xiaoming instanceof Child; // true xiaoming instanceof Parent; // true xiaoming instanceof Object; // true
Child.Prototype
,Child.Prototype
, Child.Prototype
都是 xiaoming
的原型
Child.prototype.isPrototypeOf(xiaoming); // true Parent.prototype.isPrototypeOf(xiaoming); // true Object.prototype.isPrototypeOf(xiaoming); // true
給原型添加方法,必定要放在替換原型語句後。
純粹的原型鏈繼承,雖然強大,實現起來也簡單,可是它也有些問題。
第一個問題是,包含引用類型的原型屬性。
在 重拾JS——建立對象 中說過,包含引用類型的原型屬性,會被共享(事實上,是全部原型屬性都會被共享,只是 引用類型的共享一般不是咱們所但願的)。而這也正是爲何要在構造函數中,而不是原型對象中定義屬性的緣由。
在經過原型繼承時,原型會變成另外一個類型的實例,因而,原先的實例屬性也就瓜熟蒂落的成爲了原型屬性。以下:
function Parent() { this.friend = ['aa', 'bb']; } function Child() {} Child.prototype = new Parent(); var xiaoming = new Child(); xiaoming.friend.push('cc'); console.log(xiaoming.friend) // ["aa", "bb", "cc"] var xiaohong = new Child(); console.log(xiaohong.friend) // ["aa", "bb", "cc"]
第二個問題是,在建立子類的實例時,沒法想父類傳遞參數。
所以,實踐中不多單獨使用原型鏈。
思想:在子類構造函數內部,調用父類構造函數。結合 call()
或 apply()
方法,能夠在實例上執行父類構造函數,並傳參:
function Parent() { this.friend = ['aa', 'bb']; } function Child() { // 繼承了 父類屬性 Parent.call(this); } var xiaoming = new Child(); xiaoming.friend.push('cc'); console.log(xiaoming.friend) // ["aa", "bb", "cc"] var xiaohong = new Child(); console.log(xiaohong.friend) // ["aa", "bb"]
相對於原型鏈繼承而言,借用構造函數魔術有個很大的優點,就是能夠想父類傳遞參數:
function Parent(skill) { this.skill = skill; } function Child() { // 繼承了 父類屬性。能夠實現多繼承,call多個父類對象 Parent.call(this, 'code'); // 實例屬性 this.age = 18; } var xiaoming = new Child(); console.log(xiaoming.skill) // code
既然是借用構造函數,那麼也沒法避免構造函數模式存在的問題:方法都在構造函數中定義,所以函數複用就無從談起了。
組合繼承就是同時採用 原型鏈繼承 和 構造函數繼承,發揮兩者之長,解決各自不足。
經過原型鏈實現原型屬性和方法的繼承,經過構造函數實現對實例屬性的繼承。
function Parent(skill) { this.skill = skill; this.parentAge = 56; this.friend = ['aa', 'bb'] } Parent.prototype.getParentAge = function() { return this.parentAge; }; function Child(skill, age) { // 繼承了 父類屬性(也能夠叫原型屬性) Parent.call(this, skill); // 第二次調用父類 // 實例屬性 this.childAge = age; } // 繼承方法 Child.prototype = new Parent(); // 第一次調用父類 Child.prototype.constructor = Child; // 從新指向子類 // 實例方法 Child.prototype.getChildAge = function() { return this.childAge; }; var xiaoming = new Child('code', 18); var xiaohong = new Child('sing', 16);
這種繼承方法是 JS 中最經常使用的繼承模式。
不管在什麼狀況下,都會調用兩次父類構造函數。第一次調用父類構造函數時,子類的原型
會獲得 parentAge friend 等屬性;第二次調用父類構造函數時,會在新對象
上建立了 parentAge friend 等屬性。當咱們訪問這兩個屬性時,實例中的屬性會屏蔽掉子類原型中的屬性。
要解決這個問題,可使用 寄生組合式繼承
,而這個模式又依賴 寄生式繼承
,而 寄生式繼承
又依賴 原型式繼承
,所以,先來看看 原型式繼承
和。
function object(o) { function F(){}; F.prototype = o; return new F(); }
本質上來說,object() 方法對傳入的對象執行了一次淺複製。
es5 對這種模式進行了規範化,新增 Object.create()
方法。
Object.create()
方法接收兩個參數,傳一個參數的時候,跟 object()
方法行爲相同;第二個參數跟 Object.defineProperties()
方法的第二個參數同樣,能夠經過描述符自定義每一個屬性的行爲。
var person = { name: "Nicholas", friends: ["Shelby", "Court", "Van"] }; var anotherPerson = Object.create(person, { name: { value: "Greg", } }); console.log(anotherPerson.name); // "Greg" console.log(anotherPerson.friends); // ["Shelby", "Court", "Van"]
在沒有必要興師動衆的建立構造函數,而只是想讓一個對象與另外一個對象保持相似的狀況下,原型式繼承徹底能夠勝任。
引用類型共享問題。
function createAnother(original){ var clone = object(original); clone.sayHi = function(){ alert("hi"); }; return clone; }
function object(o) { function F(){}; F.prototype = o; return new F(); } function inheritPrototype(child, parent) { var prototype = object(parent.prototype); prototype.constructor = child; child.prototype = prototype; }; function Parent(skill) { this.skill = skill; this.parentAge = 56; this.friend = ['aa', 'bb'] } Parent.prototype.getParentAge = function() { return this.parentAge; }; function Child(skill, age) { // 繼承了 父類屬性(也能夠叫原型屬性) Parent.call(this, skill); // 實例屬性 this.childAge = age; } // Child.prototype = new Parent(); inheritPrototype(Child, Parent); // Child.prototype = Object.assign({}, { ...Parent.prototype }, { constructor: Child }) // 實例方法 Child.prototype.getChildAge = function() { return this.childAge; }; var xiaoming = new Child('code', 18); var xiaohong = new Child('sing', 16);
開發人員廣泛認爲這種模式是最理想的繼承範式。
觀察代碼,發現,除了將 Child.prototype = new Parent()
替換爲 inheritPrototype(Child, Parent)
外,其餘的都同樣。
繼續查看 inheritPrototype
函數,發現它首先複製了父類的原型對象,而後將其賦值給子類的原型。
怎麼複製能父類的原型對象呢?是經過 object()
方法實現的(也能夠經過其餘方式實現,好比 Object.create()方法實現)。
思考:是否是隻要能將父類的原型對象包含在子類的原型對象中,應該就能達到相似的效果: 例如 Child.prototype = Object.assign({}, { ...Parent.prototype }, { constructor: Child })
function Parent(skill) { this.skill = skill; this.parentAge = 56; this.friend = ['aa', 'bb'] } Parent.prototype.getParentAge = function() { return this.parentAge; }; function Child(skill, age) { // 繼承了 父類屬性(也能夠叫原型屬性) Parent.call(this, skill); // 實例屬性 this.childAge = age; } Child.prototype = Object.assign({}, { ...Parent.prototype }, { constructor: Child }) // 實例方法 Child.prototype.getChildAge = function() { return this.childAge; }; var xiaoming = new Child('code', 18); var xiaohong = new Child('sing', 16);
注意,上圖是我我的理解的,不知道對不對,歡迎討論,指正。
至關於把書抄了一遍。書讀百遍,其義自見,加油。
關係圖是我的理解畫出來的,不知道正確不正確,僅供參考。