重拾JS——繼承

繼承是面相對象編程語言的一個特點,通常分爲兩類:接口繼承和實現繼承。接口繼承只繼承方法簽名,而實現繼承則繼承實際的方法。在 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);

注意,上圖是我我的理解的,不知道對不對,歡迎討論,指正。

最後

至關於把書抄了一遍。書讀百遍,其義自見,加油。

關係圖是我的理解畫出來的,不知道正確不正確,僅供參考。

相關文章
相關標籤/搜索