JS高級ES6的6種繼承方式

前言:

繼承是面向對象中老生常談的一個內容,在ECMAScript6之前,JavaScript中的繼承可謂是非常的繁瑣的,有各種各樣的繼承,本質上所有的繼承都是離不開原型鏈的,ES6新增的extends關鍵字也是通過原型鏈實現的繼承,但是語法相對來說就簡單瞭很多。

關於原型鏈的內容,可以參考上篇文章兩張圖搞懂原型鏈

本篇文章就來介紹一下在ECMAScript6之前是怎麼實現繼承的。

1.原型鏈繼承

借助於原型鏈繼承本質就是修改一下原型的指向即可,實現代碼如下:

function ParentClass() {
  this.name = '一碗周'
}
ParentClass.prototype.getName = function () {
  return this.name
}

// 定義子類,將來用於繼承父類
function ChildClass() {}

// * 將子類的原型指向父類的實例化,子類擁有父類實例化後的內容
ChildClass.prototype = new ParentClass()

// 將子類進行實例化
var child = new ChildClass()

console.log(child.getName()) // 一碗周

上面的代碼圖解如下:

圖中紅色線表示這個構造函數與實例對象的原型鏈,通過這個原型鏈的關系,從而實現瞭繼承。

這種方式實現繼承有一個缺點就是多個實例會導致原型對象上的內容時共享的,內容之間會互相影響,測試代碼如下:

function ParentClass() {
  this.colors = ['red', 'blue', 'green']
}
function ChildClass() {}

ChildClass.prototype = new ParentClass()

var child1 = new ChildClass()
var child2 = new ChildClass()
console.log(child1.colors) // [ 'red', 'blue', 'green' ]

child2.colors.push('black')
console.log(child2.colors) // [ 'red', 'blue', 'green', 'black' ]

console.log(child1.colors) // [ 'red', 'blue', 'green', 'black' ]

測試代碼中的child1並沒有進行修改,但是修改瞭child1之後,child1中的值也發生瞭改變。

2.借助構造函數繼承

所謂的借助構造函數繼承(有些資料也稱為偽造對象或經典繼承),就是通過子對象借助Function.call()或者Function.apply()方法調用父類構造函數完成繼承,

示例代碼如下所示:

function Parent() {
  // 父級對象

  this.parent = 'parent'
}

Parent.prototype.name = '一碗周' // 為 Parent 父級對象的原型增加屬性

function Child() {
  // 子級對象

  this.child = 'child'

  Parent.call(this) // 使用 call() 或者 apply() 方法調用父級構造函數 實現繼承。
}

const child = new Child()

console.log(child)

console.log(child.name) // undefined     // 不會繼承父類的原型

執行流程如下所示:

使用這種方式的優點是避免瞭引用類型的實例被所有對象共享,缺點是因為所有的方法都定義在瞭構造函數中,是不會繼承原型對象,而且每實例化一個對象之後都會重新創建一遍這些方法,占用內存空間,更別說函數復用瞭。

3.組合式繼承

之前掌握的兩種繼承方式都是存在缺點的,基於原型繼承的繼承方式,所有實例化後的對象都共享原型的方法和屬性,如果有一個更改則都會進行更改。而借助構造函數繼承的方式又無法繼承原型屬性。所以就出現瞭結合式繼承,就是將基於原型繼承方式和借助構造函數的繼承方式結合起來,取其精華去其糟粕的一種繼承方式。

實現組合式繼承的基本思路如下:

  • 使用原型鏈或原型式繼承實現對原型的屬性和方法的繼承。
  • 通過結構構造函數實現對實例對象的屬性的繼承。

這樣,既通過在原型上定義方法實現瞭函數的復用,又可以保證每個對象都有自己的專有屬性。

示例代碼如下所示:

// 父級對象
function Parent() {
  this.parent = 'parent'
}
// 為 Parent 父級對象的原型增加屬性
Parent.prototype.name = '一碗周'
// 子級對象
function Child() {
  this.child = 'child'
  // 使用 call() 或者 apply() 方法調用父級構造函數 實現繼承。
  Parent.call(this)
}
// 解決不會繼承構造函數的原型對象的問題
Child.prototype = Parent.prototype

const child = new Child()
console.log(child.name) // 一碗周

4.原型式繼承

我們可以使用Object.create()方法實現一種繼承,實例代碼如下:

var person = {
  name: '一碗周',
  friends: ['張三', '李四', '王五'],
}

var anotherPerson = Object.create(person)
anotherPerson.name = '一碗甜'
anotherPerson.friends.push('趙六')

console.log(person.friends) // [ '張三', '李四', '王五', '趙六' ]

該方式的缺點與第一種一樣,都是多個實例會導致原型對象上的內容時共享的,內容之間會互相影響。

5.寄生式繼承

寄生式繼承的基礎是在原型式繼承的的基礎上,增強對象,返回構造函數,實例代碼如下:

var person = {
  name: '一碗周',
  friends: ['張三', '李四', '王五'],
}

function createAnother(original) {
  var clone = Object.create(original) // 通過調用 object() 函數創建一個新對象
  clone.sayMe = function () {
    // 以某種方式來增強對象
  }
  return clone // 返回這個對象
}

var anotherPerson = createAnother(person)
anotherPerson.sayMe()

它的缺點與原生式繼承是一樣的。

6.寄生組合式繼承

該繼承方式是借助構造函數傳遞參數和寄生式繼承所實現的,實例代碼如下:

function inheritPrototype(ChildClass, ParentClass) {
  var prototype = Object.create(ParentClass.prototype) // 創建對象,創建父類原型的一個副本
  // 修改創建的父類原型副本的 constructor 並將子類的 prototype 指向這個類,形成與父類無關聯的類
  prototype.constructor = ChildClass
  ChildClass.prototype = prototype
}

// 父類初始化實例屬性和原型屬性
function ParentClass(name) {
  this.name = name
  this.colors = ['red', 'blue', 'green']
}
ParentClass.prototype.sayName = function () {
  console.log(this.name)
}

// 借用構造函數傳遞增強子類實例屬性(支持傳參和避免篡改)
function ChildClass(name, age) {
  // 拷貝父類所有自有屬性
  ParentClass.call(this, name)
  this.age = age
}

// 將父類原型指向子類
inheritPrototype(ChildClass, ParentClass)

// 新增子類原型屬性
ChildClass.prototype.sayAge = function () {
  console.log(this.age)
}

var instance1 = new ChildClass('一碗周', 19)
var instance2 = new ChildClass('一碗甜', 18)

instance1.colors.push('black')
console.log(instance1.colors) // [ 'red', 'blue', 'green', 'black' ]
instance1.sayName() // 一碗周
instance2.colors.push('yellow')
console.log(instance2.colors) // [ 'red', 'blue', 'green', 'yellow' ]

這個例子的高效率體現在它隻調用瞭一次ParentClass構造函數,並且因此避免瞭在ChildClass.prototype上創建不必要的、多餘的屬性。於此同時,原型鏈還能保持不變;因此,還能夠正常使用instanceofisPrototypeOf()。

如果你沒有看懂,那就繼續看,首先,我們將核心代碼進行抽離,如下圖:

上圖中就是我們的核心代碼,然後我們來看一下默認的ParentClassChildClass的原型鏈是什麼樣子的,

圖如下:

然後我們調用inheritPrototype()方法,並將修改ChildClass的原型,解析圖如下:

最後不要忘記瞭在子類中通過call()方法調用父類,從而實現copy父類的自有屬性,至此就實現瞭一個比較完善的繼承方式。

結語:

這篇文章介紹瞭除extends關鍵字之外的六種繼承方式,雖然說在ECMAScript6中新增瞭class關鍵字以及類相關的所有內容,文本介紹的繼承方式都已經不怎麼使用瞭。

但是ECMAScript6新增的類本質上就是語法糖,隻要是JavaScript談論繼承,始終離不開class關鍵字。

到此這篇關於JS高級ES6的6種繼承方式的文章就介紹到這瞭,更多相關ES6的6種繼承方式內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: