天天看點

好程式員分享JavaScript六種繼承方式詳解

好程式員分享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将會覆寫這個方法,達到重新的目的。 但是這個方法依然存在于父類中

記住不能以字面量的形式添加,因為,上面說過通過執行個體繼承本質上就是重寫,再使用字面量形式,又是一次重寫了,但這次重寫沒有跟父類有任何關聯,是以就會導緻原型鍊截斷

this.property = true           
return this.property           
this.subproperty = false           

SubType.prototype = {

getSubValue: function() {
    return this.subproperty
}           

var instance = new SubType() console.log(instance.getSuperValue()) // error

問題

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

this.colors = ['red', 'blue', 'green']           

function SubType() {}

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()方法來改變對象的執行上下文

this.colors = ['red', 'blue', 'green']           
// 繼承SuperType
SuperType.call(this)           

console.log(instance2.colors) // ["red", "blue", "green"]

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

結果,SubType的每個執行個體就會具有自己的colors屬性的副本了

傳遞參數

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

function SuperType(name) {

this.name = name           
// 繼承SuperType
SuperType.call(this, 'Jiang')

this.job = 'student'           

var instance = new SubType() console.log(instance.name) // Jiang

console.log(instance.job) // student

如果僅僅借助構造函數,方法都在構造函數中定義,是以函數無法達到複用

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

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

思路就是使用原型鍊實作對原型屬性和方法的繼承,而通過借用構造函數來實作對執行個體屬性的繼承

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

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不能接受第二個參數以外

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 // 傳回這個對象           
name: 'Jiang'           

var anotherPeson = createAnother(person) anotherPeson.sayHi()

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

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

寄生組合式繼承

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

父類

this.name = name

this.colors = ['red', 'blue', 'green']

第一次在子類構造函數中

// 繼承屬性

SuperType.call(this, name)

this.job = job

第二次将子類的原型指向父類的執行個體

當使用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屬性,第三部将子類的原型指向這個副本

this.name = name this.colors = ['red', 'blue', 'green']           
console.log(this.name)           
// 繼承屬性
SuperType.call(this, name)

this.job = job           

// 繼承

inheritPrototype(SubType, SuperType)

var instance = new SubType('Jiang', 'student') instance.sayName()

補充:直接使用Object.create來實作,其實就是将上面封裝的函數拆開,這樣示範可以更容易了解
this.name = name this.colors = ['red', 'blue', 'green']           
console.log(this.name)           
// 繼承屬性
SuperType.call(this, name)

this.job = job           

SubType.prototype = Object.create(SuperType.prototype)

// 修複constructor

SubType.prototype.constructor = SubType

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

繼續閱讀