關於JavaScript繼承的幾種方法

前言

繼承是JavaScript面向對象編程非常重要的一個特性,基本在日常使用以及面試過程中都會使用到。查閱一些資料後整理以下幾種繼承的方法記錄在自己的博客中。

如何實現繼承

原型鏈繼承

原型鏈繼承是最簡單的一種繼承方式,只需要將子類的prototype值等於父類的實例即可

  function Animal(){
    this.superType = 'Animal'
  }
  Animal.prototype.getSuperType = function(){
      console.log(this.superType);
  }
  function Cat(name){
    this.name = name;
    this.type = 'Cat';
  }
  // 原型繼承
  Cat.prototype = new Animal();

  var cat = new Cat()

  cat.getSuperType(); // 控制檯輸出Animal

以上代碼是將Animal的實例覆蓋Cat的原型,本質是重寫原型對象,代之一個新類型的實例。使Cat擁有Animal實例的所有屬性和方法(getSuperType爲原型方法),並且還有個指針指向了Animal的原型。當創建Cat的實例cat時,cat指向的是Cat的原型,Cat的原型又指向Animal的原型。

缺點

  • 引用類型值的原型屬性會被共享
  • 在創建子類型的實例時,無法向超類型的構造函數傳遞參數

構造函數繼承

借用構造函數繼承也是非常簡單地一種繼承方式,即在子類構造函數的內部調用父類型構造函數。代碼實現爲:

function Animal(name){
  this.name = name;
}
Animal.prototype.getName = function(){
  console.log(this.name);
}
function Cat(){
  Animal.call(this, "cat");
  this.eat = "fish"
}

var instance = new Cat();
console.log(instance.name);
console.log(instance.eat); // 控制檯相繼輸出 cat fish
instance.getName() // error getName undefined

以上代碼中的 Animal 只接受一個參數 name ,該參數會直接賦給一個屬性。在 Cat 構造函數內部調用 Animal 構造函數時,實際上視爲 Cat 的實例設置了 name 屬性。爲了確保 Animal 構造函數不會重寫子類的屬性,可以在調用父類構造函數後,再添加應該在子類型中定義的屬性。

優點

  • 引用類型的原型屬性不會被共享
  • 可以在子類型構造函數中向父類構造函數傳遞參數

缺點

  • 方法都在構造函數中定義,函數無法複用
  • 在父類的原型定義的方法,對子類型是不可見,導致所有的類型都只能使用構造函數

組合繼承

組合繼承,也叫做僞經典繼承,指的是將原型鏈和借用構造函數組合到一塊

function SuperType(name) {
  this.name = name
  this.colors = ["red", "blue", "green"]
}
SuperType.prototype.getName = function() {
  console.log(this.name)
}
function SubType(name, age) {
  // 繼承屬性
  SuperType.call(this, name)
  this.age = age
}
// 繼承方法
SubType.prototype = new SuperType()

SubType.prototype.sayAge = function () {
  console.log(this.age)
}
let instance1 = new SubType("tom", "8")
instance1.colors.push("black")
console.log(instance1.colors) // red, blue, green, black
instance1.getName()  // tom
instance1.sayAge()  // 8

let instance2 = new SubType("jerry", "9")
console.log(instance2.colors) // red, blue, green
instance2.getName() // jerry
instance2.sayAge()  // 9

此例子中, SuperType 構造函數定義了兩個屬性: name colors SuperType 定義了一個方法 sayName() SubType 構造函數在調用 SuperType 構造函數時傳入 name 參數,緊接着又定義了它自己的屬性 age 。然後將 SuperType 的實例賦值給 SubType 的原型,然後又在該新原型上定義了方法 sayAge 。這樣就可以讓兩個不同的 SubType 實例即擁有自己的屬性,又可以使用相同的方法。
組合繼承避免了原型鏈和借用構造函數的缺陷,融合了它們的優點,成爲 JavaScript 中最常用的繼承模式。而且,instanceofisPortotypeOf() 也能夠用於識別組合繼承創建的對象。

缺點

  • 會調用兩次超類構造函數,並且會分別在實例和原型上有重複的屬性

原型式繼承

原型式繼承:其思想是藉助原型,可以基於已有的對象創建新的對象,同時還不用創建自定義類。以下代碼,在 object() 函數內部,先創建了一個臨時性的構造函數,然後將傳入的對象作爲這個構造函數原型,最後返回了這個臨時類型的一個新實例。從本質上講, object() 對傳入其中的對象執行了一次淺複製。

function object(o) {
  function F(){}
  F.prototype = o
  return new F()
}
var person = {
  name: 'Tom',
  friends: ['Jerry', 'Sherry', 'Harry']
}
var anotherPerson = object(person)
anotherPerson.name = 'Greg'
anotherPerson.friends.push('Rob')

var yetAnotherPerson = object(person)
yetAnotherPerson.name = 'Linda'
yetAnotherPerson.friends.push('Bob')
console.log(person.friends) // Jerry, Sherry, Harry, Rob, Bob

還可以使用 ES5 新增的 Object.create() 方法進行創建

var anotherPerson = Object.create(person)
anotherPerson.name = 'Greg'
anotherPerson.friends.push('Rob')

var yetAnotherPerson = Object.create(person)
yetAnotherPerson.name = 'Linda'
yetAnotherPerson.friends.push('Bob')
console.log(person.friends) // Jerry, Sherry, Harry, Rob, Bob

如果只是想讓一個對象與另一個對象保持類似的情況下,原型式繼承是可以完全勝任的

寄生式繼承

寄生式繼承的思路與構造函數和工廠模式類似,即創建一個僅用於封裝繼承過程的函數,該函數在內部以某種方式來增強對象,最後再返回對象

function createAnother(original) {
  var clone = Object.create(original)
  clone.sayHi = function () {
    console.log('Hi')
  }
  return clone
}

上述代碼中, createAnother() 函數接收一個參數,也就是將要作爲新對象基礎的對象。然後,把 original 傳遞給 object 函數,將返回的結果賦值給clone。再爲 clone 對象添加一個新方法 sayHi() ,最後返回 clone 對象。

var person = {
  name: 'Tom',
  friends: ['Jerry', 'Sherry', 'Harry']
}
var anotherPerson = createAnother(person)
anotherPerson.sayHi() // Hi

在主要考慮對象而不是自定義類型和構造函數的情況下,寄生式繼承也是一種有用的模式。

寄生組合式繼承

寄生組合式繼承,即通過借用構造函數來繼承屬性,通過原型鏈的混成形式來繼承方法。其背後的基本思路是:不必爲指定子類行的原型而調用超類型的構造函數,我們所需要的無非就是超類型原型的一個副本而已。本質上,就是使用寄生式繼承來繼承超類型的原型,然後再將結果指定給子類型的原型。代碼如下:

function inheritPrototype(subType, superType) {
  var prototype = Object.create(superType.prototype)
  prototype.constructor = subType
  subType.prototype = prototype
}
function SuperType(name) {
  this.name = name
  this.colors = ['red', 'blue', 'green']
}

SuperType.prototype.sayName = function () {
  console.log(this.name)
}

function SubType (name, age) {
  SuperType.call(this, name)
  this.age = age
}

inheritPrototype(subType, SuperType)

SubType.prototype.sayAge = function () {
  console.log(this.age)
}

該代碼只調用了一次 SuperType 構造函數,並且因此避免了在 SubType.prototype 上創建不必要的,多餘的屬性。與此同時,原型鏈還能保持不變。屬於最理想的繼承範式。

總結

繼承是 JS 中極爲重要的一塊知識,目前只能參考資料將這些記錄下來,完全喫透還需要慢慢實戰。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章