JavaScript 六種繼承方式

作者:Xuthus
xxxgitone.github.io/2017/06/12/JavaScript六種繼承方式/

繼承是面向對象編程中又一非常重要的概念,JavaScript支持實現繼承,不支持接口繼承,實現繼承主要依靠原型鏈來實現的。

原型鏈

首先得要明白什麼是原型鏈,在一篇文章看懂proto和prototype的關係及區別中講得非常詳細

原型鏈繼承基本思想就是讓一個原型對象指向另一個類型的實例

function SuperType() {
  this.property = true
}
SuperType.prototype.getSuperValue = function () {
  return this.property
}
function SubType() {
  this.subproperty = false
}
SubType.prototype = new SuperType()
SubType.prototype.getSubValue = function () {
  return this.subproperty
}
var instance = new SubType()
console.log(instance.getSuperValue()) // true

代碼定義了兩個類型SuperType和SubType,每個類型分別有一個屬性和一個方法,SubType繼承了SuperType,而繼承是通過創建SuperType的實例,並將該實例賦給SubType.prototype實現的。

實現的本質是重寫原型對象,代之以一個新類型的實例,那麼存在SuperType的實例中的所有屬性和方法,現在也存在於SubType.prototype中了。

我們知道,在創建一個實例的時候,實例對象中會有一個內部指針指向創建它的原型,進行關聯起來,在這裏代碼SubType.prototype = new SuperType(),也會在SubType.prototype創建一個內部指針,將SubType.prototype與SuperType關聯起來。

所以instance指向SubType的原型,SubType的原型又指向SuperType的原型,繼而在instance在調用getSuperValue()方法的時候,會順着這條鏈一直往上找。

添加方法
在給SubType原型添加方法的時候,如果,父類上也有同樣的名字,SubType將會覆蓋這個方法,達到重新的目的。 但是這個方法依然存在於父類中。

記住不能以字面量的形式添加,因爲,上面說過通過實例繼承本質上就是重寫,再使用字面量形式,又是一次重寫了,但這次重寫沒有跟父類有任何關聯,所以就會導致原型鏈截斷。

function SuperType() {
  this.property = true
}
SuperType.prototype.getSuperValue = function () {
  return this.property
}
function SubType() {
  this.subproperty = false
}
SubType.prototype = new SuperType()
SubType.prototype.getSubValue = function () {
  return this.subproperty
}
var instance = new SubType()
console.log(instance.getSuperValue()) // true

代碼定義了兩個類型SuperType和SubType,每個類型分別有一個屬性和一個方法,SubType繼承了SuperType,而繼承是通過創建SuperType的實例,並將該實例賦給SubType.prototype實現的。

實現的本質是重寫原型對象,代之以一個新類型的實例,那麼存在SuperType的實例中的所有屬性和方法,現在也存在於SubType.prototype中了。

我們知道,在創建一個實例的時候,實例對象中會有一個內部指針指向創建它的原型,進行關聯起來,在這裏代碼SubType.prototype = new SuperType(),也會在SubType.prototype創建一個內部指針,將SubType.prototype與SuperType關聯起來。

所以instance指向SubType的原型,SubType的原型又指向SuperType的原型,繼而在instance在調用getSuperValue()方法的時候,會順着這條鏈一直往上找。

添加方法

在給SubType原型添加方法的時候,如果,父類上也有同樣的名字,SubType將會覆蓋這個方法,達到重新的目的。 但是這個方法依然存在於父類中。

記住不能以字面量的形式添加,因爲,上面說過通過實例繼承本質上就是重寫,再使用字面量形式,又是一次重寫了,但這次重寫沒有跟父類有任何關聯,所以就會導致原型鏈截斷。

function SuperType() {
  this.property = true
}
SuperType.prototype.getSuperValue = function () {
  return this.property
}
function SubType() {
  this.subproperty = false
}
SubType.prototype = new SuperType()
SubType.prototype = {
  getSubValue:function () {
   return this.subproperty
  }
}
var instance = new SubType()
console.log(instance.getSuperValue())  // error

問題

單純的使用原型鏈繼承,主要問題來自包含引用類型值的原型。

function SuperType() {
  this.colors = ['red', 'blue', 'green']
}
function SubType() {
}
SubType.prototype = new SuperType()
var instance1 = new SubType()
var instance2 = new SubType()
instance1.colors.push('black')
console.log(instance1.colors)  // ["red", "blue", "green", "black"]
console.log(instance2.colors) // ["red", "blue", "green", "black"]

在SuperType構造函數定義了一個colors屬性,當SubType通過原型鏈繼承後,這個屬性就會出現SubType.prototype中,就跟專門創建了SubType.prototype.colors一樣,所以會導致SubType的所有實例都會共享這個屬性,所以instance1修改colors這個引用類型值,也會反映到instance2中。

借用構造函數

此方法爲了解決原型中包含引用類型值所帶來的問題。

這種方法的思想就是在子類構造函數的內部調用父類構造函數,可以藉助apply()和call()方法來改變對象的執行上下文


function SuperType() {
  this.colors = ['red', 'blue', 'green']
}
function SubType() {
  // 繼承SuperType
  SuperType.call(this)
}
var instance1 = new SubType()
var instance2 = new SubType()
instance1.colors.push('black')
console.log(instance1.colors)  // ["red", "blue", "green", "black"]
console.log(instance2.colors) // ["red", "blue", "green"]

在新建SubType實例是調用了SuperType構造函數,這樣以來,就會在新SubType對象上執行SuperType函數中定義的所有對象初始化代碼。

結果,SubType的每個實例就會具有自己的colors屬性的副本了。

傳遞參數

藉助構造函數還有一個優勢就是可以傳遞參數


function SuperType(name) {
  this.name = name
}
function SubType() {
  // 繼承SuperType
  SuperType.call(this, 'Jiang')

  this.job = 'student'
}
var instance = new SubType()
console.log(instance.name)  // Jiang
console.log(instance.job)   // student

問題

如果僅僅藉助構造函數,方法都在構造函數中定義,因此函數無法達到複用

組合繼承(原型鏈+構造函數)

組合繼承是將原型鏈繼承和構造函數結合起來,從而發揮二者之長的一種模式。

思路就是使用原型鏈實現對原型屬性和方法的繼承,而通過借用構造函數來實現對實例屬性的繼承。

這樣,既通過在原型上定義方法實現了函數複用,又能夠保證每個實例都有它自己的屬性。


function SuperType(name) {
  this.name = name
  this.colors = ['red', 'blue', 'green']
}
SuperType.prototype.sayName = function () {
  console.log(this.name)
}
function SubType(name, job) {
  // 繼承屬性
  SuperType.call(this, name)

  this.job = job
}
// 繼承方法
SubType.prototype = new SuperType()
SubType.prototype.constructor = SuperType
SubType.prototype.sayJob = function() {
  console.log(this.job)
}
var instance1 = new SubType('Jiang', 'student')
instance1.colors.push('black')
console.log(instance1.colors) //["red", "blue", "green", "black"]
instance1.sayName() // 'Jiang'
instance1.sayJob()  // 'student'
var instance2 = new SubType('J', 'doctor')
console.log(instance2.colors) // //["red", "blue", "green"]
instance2.sayName()  // 'J'
instance2.sayJob()  // 'doctor'

這種模式避免了原型鏈和構造函數繼承的缺陷,融合了他們的優點,是最常用的一種繼承模式。

原型式繼承

藉助原型可以基於已有的對象創建新對象,同時還不必因此創建自定義類型。


function object(o) {
  function F() {}
  F.prototype = o
  return new F()
}

在object函數內部,先創建一個臨時性的構造函數,然後將傳入的對象作爲這個構造函數的原型,最後返回這個臨時類型的一個新實例。

本質上來說,object對傳入其中的對象執行了一次淺複製。


var person = {
  name: 'Jiang',
  friends: ['Shelby', 'Court']
}
var anotherPerson = object(person)
console.log(anotherPerson.friends)  // ['Shelby', 'Court']

這種模式要去你必須有一個對象作爲另一個對象的基礎。

在這個例子中,person作爲另一個對象的基礎,把person傳入object中,該函數就會返回一個新的對象。

這個新對象將person作爲原型,所以它的原型中就包含一個基本類型和一個引用類型。

所以意味着如果還有另外一個對象關聯了person,anotherPerson修改數組friends的時候,也會體現在這個對象中。

Object.create()方法

ES5通過Object.create()方法規範了原型式繼承,可以接受兩個參數,一個是用作新對象原型的對象和一個可選的爲新對象定義額外屬性的對象,行爲相同,基本用法和上面的object一樣,除了object不能接受第二個參數以外。

var person = {
  name: 'Jiang',
  friends: ['Shelby', 'Court']
}
var anotherPerson = Object.create(person)
console.log(anotherPerson.friends)  // ['Shelby', 'Court']

寄生式繼承

寄生式繼承的思路與寄生構造函數和工廠模式類似,即創建一個僅用於封裝繼承過程的函數。

function createAnother(o) {
  var clone = Object.create(o) // 創建一個新對象
  clone.sayHi = function() { // 添加方法
    console.log('hi')
  }
  return clone  // 返回這個對象
}
var person = {
  name: 'Jiang'
}
var anotherPeson = createAnother(person)
anotherPeson.sayHi()

基於person返回了一個新對象anotherPeson,新對象不僅擁有了person的屬性和方法,還有自己的sayHi方法。

在主要考慮對象而不是自定義類型和構造函數的情況下,這是一個有用的模式。

寄生組合式繼承

在前面說的組合模式(原型鏈+構造函數)中,繼承的時候需要調用兩次父類構造函數。

父類


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

第一次在子類構造函數中

function SubType(name, job) {
  // 繼承屬性
  SuperType.call(this, name)

  this.job = job
}

第二次將子類的原型指向父類的實例

// 繼承方法
SubType.prototype = new SuperType()

當使用var instance = new SubType()的時候,會產生兩組name和color屬性,一組在SubType實例上,一組在SubType原型上,只不過實例上的屏蔽了原型上的。

使用寄生式組合模式,可以規避這個問題。

這種模式通過借用構造函數來繼承屬性,通過原型鏈的混成形式來繼承方法。

基本思路:不必爲了指定子類型的原型而調用父類的構造函數,我們需要的無非就是父類原型的一個副本。

本質上就是使用寄生式繼承來繼承父類的原型,在將結果指定給子類型的原型。

function inheritPrototype(subType, superType) {
  var prototype = Object.create(superType.prototype)
  prototype.constructor = subType
  subType.prototype = prototype
}

該函數實現了寄生組合繼承的最簡單形式。

這個函數接受兩個參數,一個子類,一個父類。

第一步創建父類原型的副本,第二步將創建的副本添加constructor屬性,第三部將子類的原型指向這個副本。

 function SuperType(name) {
  this.name = name
  this.colors = ['red', 'blue', 'green']
}
SuperType.prototype.sayName = function () {
  console.log(this.name)
}
function SubType(name, job) {
  // 繼承屬性
  SuperType.call(this, name)

  this.job = job
}
// 繼承
inheritPrototype(SubType, SuperType)
var instance = new SubType('Jiang', 'student')
instance.sayName()

補充:直接使用Object.create來實現,其實就是將上面封裝的函數拆開,這樣演示可以更容易理解。

function SuperType(name) {
  this.name = name
  this.colors = ['red', 'blue', 'green']
}
SuperType.prototype.sayName = function () {
  console.log(this.name)
}
function SubType(name, job) {
  // 繼承屬性
  SuperType.call(this, name)

  this.job = job
}
// 繼承
SubType.prototype = Object.create(SuperType.prototype)
// 修復constructor
SubType.prototype.constructor = SubType
var instance = new SubType('Jiang', 'student')
instance.sayName()

ES6新增了一個方法,Object.setPrototypeOf,可以直接創建關聯,而且不用手動添加constructor屬性。

// 繼承
Object.setPrototypeOf(SubType.prototype, SuperType.prototype)
console.log(SubType.prototype.constructor === SubType) // true
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章