天天看點

JS 常見的 6 種繼承方式

JS 常見的 6 種繼承方式

第一種:原型鍊繼承

原型鍊繼承是比較常見的繼承方式之一,其中涉及的構造函數、原型和執行個體,三者之間存在着一定的關系,即每一個構造函數都有一個原型對象,原型對象又包含一個指向構造函數的指針,而執行個體則包含一個原型對象的指針。

function Parent1() {
  this.name = 'parent1'
  this.play = [1, 2, 3]
}

function Child1() {
  this.type = 'child1'
}

Child1.prototype = new Parent1()

let child1 = new Child1()

console.log(child1)
           

上面的代碼看似沒有問題,雖然父類的方法和屬性都能夠通路,但其實有一個潛在的問題

let c1 = new Child1();

  let c2 = new Child1();

  c1.play.push(4);

  console.log(c1.play, c2.play);

           

這段代碼在控制台執行之後,可以看到結果如下:

JS 常見的 6 種繼承方式

明明隻改變了 c1 的 play 屬性,為什麼 c2 也跟着變了呢?原因很簡單,因為兩個執行個體使用的是同一個原型對象。它們的記憶體空間是共享的,當一個發生變化的時候,另外一個也随之進行了變化,這就是使用原型鍊繼承方式的一個缺點。

第二種:構造函數繼承(借助 call)

function Parent1() {
  this.name = 'parent1'
}

Parent1.prototype.getName = function () {
  return this.name
}

function Child1() {
  // 借助call方法改變this指向,進而将Parent中的屬性添加至Child中
  Parent1.call(this)
  this.type = 'child1'
}

let child = new Child1()

// 正常運作
console.log(child)

// 運作報錯,因為child沒有getName方法
console.log(child.getName())

           

運作結果如下

JS 常見的 6 種繼承方式

從上面的結果就可以看到構造函數實作繼承的優缺點,它使父類的引用屬性不會被共享,優化了第一種繼承方式的弊端;但是随之而來的缺點也比較明顯——隻能繼承父類的執行個體屬性和方法,不能繼承原型屬性或者方法。

第三種:組合繼承(前兩種組合)

這種方式結合了前兩種繼承方式的優缺點,結合起來的繼承,代碼如下

function Parent3() {
  this.name = 'parent3'
  this.play = [1, 2, 3]
}

Parent3.prototype.getName = function () {
  return this.name
}

function Child3() {
  // 第二次調用Parent
  Parent3.call(this)
  this.type = 'child3'
}

// 第一次調用Parent
Child3.prototype = new Parent3()

// 手動挂上構造器,指向自己的構造函數
Child3.prototype.constructor = Child3

let c1 = new Child3()
let c2 = new Child3()
c1.play.push(4)
console.log(c1.play, c2.play) // 不互相影響
console.log(c1.getName()) // 正常輸出'parent3'
console.log(c2.getName()) // 正常輸出'parent3'

           

執行上面的代碼,可以看到控制台的輸出結果,之前方法一和方法二的問題都得以解決。

JS 常見的 6 種繼承方式

但是這裡又增加了一個新問題:通過注釋我們可以看到 Parent3 執行了兩次,第一次是改變Child3 的 prototype 的時候,第二次是通過 call 方法調用 Parent3 的時候,那麼 Parent3 多構造一次就多進行了一次性能開銷,這是我們不願看到的。

上面介紹的更多是圍繞着構造函數的方式,那麼對于 JavaScript 的普通對象,怎麼實作繼承呢?

第四種:原型式繼承

這裡不得不提到的就是 ES5 裡面的 Object.create 方法,這個方法接收兩個參數:一是用作新對象原型的對象、二是為新對象定義額外屬性的對象(可選參數)。

let parent4 = {
  name: 'parent4',
  friends: ['p1', 'p2', 'p3'],
  getName: function () {
    return this.name
  },
}

let person = Object.create(parent4)

person.name = 'Tom'
person.friends.push('jerry')

let person2 = Object.create(parent4)
person2.friends.push('lucy')

console.log(person.name)
console.log(person.name === person.getName())
console.log(person2.name)
console.log(person.friends)
console.log(person2.friends)

           

通過 Object.create 這個方法可以實作普通對象的繼承,不僅僅能繼承屬性,同樣也可以繼承 getName 的方法,請看這段代碼的執行結果。

JS 常見的 6 種繼承方式

最後兩個輸出結果是一樣的,講到這裡你應該可以聯想到淺拷貝的知識點,關于引用資料類型“共享”的問題,其實 Object.create 方法是可以為一些對象實作淺拷貝的。

那麼關于這種繼承方式的缺點也很明顯,多個執行個體的引用類型屬性指向相同的記憶體,存在篡改的可能,接下來我們看一下在這個繼承基礎上進行優化之後的另一種繼承方式——寄生式繼承。

第五種:寄生式繼承

使用原型式繼承可以獲得一份目标對象的淺拷貝,然後利用這個淺拷貝的能力再進行增強,添加一些方法,這樣的繼承方式就叫作寄生式繼承。

雖然其優缺點和原型式繼承一樣,但是對于普通對象的繼承方式來說,寄生式繼承相比于原型式繼承,還是在父類基礎上添加了更多的方法。

let parent5 = {
  name: 'parent5',
  friends: ['p1', 'p2', 'p3'],
  getName: function () {
    return this.name
  },
}

function clone(original) {
  let clone = Object.create(original)
  clone.getFriends = function () {
    return this.friends
  }
  return clone
}

let person = clone(parent5)

console.log(person.getName())
console.log(person.getFriends())

           

通過上面這段代碼,我們可以看到 person 是通過寄生式繼承生成的執行個體,它不僅僅有 getName 的方法,而且可以看到它最後也擁有了 getFriends 的方法

JS 常見的 6 種繼承方式

從最後的輸出結果中可以看到,person 通過 clone 的方法,增加了 getFriends 的方法,進而使 person 這個普通對象在繼承過程中又增加了一個方法,這樣的繼承方式就是寄生式繼承。

在上面第三種組合繼承方式中提到了一些弊端,即兩次調用父類的構造函數造成浪費,下面要介紹的寄生組合繼承就可以解決這個問題。

第六種:寄生組合式繼承

結合第四種中提及的繼承方式,解決普通對象的繼承問題的 Object.create 方法,我們在前面這幾種繼承方式的優缺點基礎上進行改造,得出了寄生組合式的繼承方式,這也是所有繼承方式裡面相對最優的繼承方式

function Parent6() {
  this.name = 'parent6'
  this.play = [1, 2, 3]
}
Parent6.prototype.getName = function () {
  return this.name
}

function Child6() {
  Parent6.call(this)
  this.friends = 'child5'
}

function clone(parent, child) {
  child.prototype = Object.create(parent.prototype)
  child.prototype.constructor = child
}

clone(Parent6, Child6)

Child6.prototype.getFriends = function () {
  return this.friends
}

let person = new Child6()
console.log(person)
console.log(person.getName())
console.log(person.getFriends())

           

通過這段代碼可以看出來,這種寄生組合式繼承方式,基本可以解決前幾種繼承方式的缺點,較好地實作了繼承想要的結果,同時也減少了構造次數,減少了性能的開銷,我們來看一下上面這一段代碼的執行結果。

JS 常見的 6 種繼承方式

整體看下來,這六種繼承方式中,寄生組合式繼承是這六種裡面最優的繼承方式。另外,ES6 還提供了繼承的關鍵字 extends,通過ES6轉ES5的方式可以看到,其底層實作邏輯也是采用了寄生組合式繼承,是以也證明了這種方式是較優的解決繼承的方式。

總結

JS 常見的 6 種繼承方式

作者:若離

連結:https://kaiwu.lagou.com/course/courseInfo.htm?courseId=601#/detail/pc?id=6176

來源:拉鈎教育-JavaScript 核心原理精講

繼續閱讀