好程式設計師分享JavaScript六種繼承方式詳解

好程式設計師IT發表於2019-04-10

好程式設計師 分享 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()) // 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)

寄生式繼承

 

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

 

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 屬性


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69913892/viewspace-2640961/,如需轉載,請註明出處,否則將追究法律責任。

相關文章