JS實(shí)現(xiàn)最佳繼承

? 繼承是OO語(yǔ)言一個(gè)最為津津樂(lè)道的概念,JS繼承主要依靠的是原型鏈的原理來(lái)實(shí)現(xiàn)的。關(guān)于原型鏈的內(nèi)容,有不明白的可以閱讀 《構(gòu)造函數(shù)與原型鏈》一文。

# 純?cè)玩溊^承

? 簡(jiǎn)單的說(shuō),繼承的基本思想就是利用原型讓一個(gè)引用類(lèi)型繼承另一個(gè)引用類(lèi)型的屬性和方法。為了方便理解,舉例如下:

function Super() {
  this.name = 'zfs'
}
Super.prototype.sayName = function() {
  return this.name
}
function Sub() {
  this.sex = 'male'
}
// 繼承了 Super,解釋見(jiàn)下方
Sub.prototype = new Super()

var person = new Sub()

console.log(person.sayName())

? 以上代碼定義了兩個(gè)類(lèi)型 SuperSub,后者重寫(xiě)了原型對(duì)象為Super的實(shí)例,這意味著,Super的所有實(shí)例屬性和原型屬性都將成為Sub的原型屬性,那么任意一個(gè)由Sub實(shí)例化出來(lái)的實(shí)例,自然就能訪問(wèn)到Super的所有屬性,從而實(shí)現(xiàn)繼承。其關(guān)系圖如下:


? 在上面的代碼中,我們沒(méi)有使用Sub默認(rèn)的原型對(duì)象,而是將其更換成Super的實(shí)例。那么此時(shí)person.constructor指向的是Super而不是Sub,原因是因?yàn)樵谠蛯?duì)象中的constructor也被重寫(xiě)了。

所有的函數(shù)的默認(rèn)原型都是Object的實(shí)例,因此,默認(rèn)原型內(nèi)部都會(huì)包含一個(gè)指針指向Object.prototype,這就是為什么自定義類(lèi)型會(huì)有toString()、valueOf()這些方法的原因。

【缺陷】
(1)因原型屬性共享于所有實(shí)例之中,當(dāng)在實(shí)例中需要對(duì)超類(lèi)中某個(gè)屬性進(jìn)行更新時(shí),必然會(huì)影響到所有父類(lèi)的其他實(shí)例,造成數(shù)據(jù)污染
(2)在創(chuàng)建子類(lèi)的實(shí)例時(shí),不能向超類(lèi)的構(gòu)造函數(shù)傳遞參數(shù)。
?

# 構(gòu)造函數(shù)式繼承

? 這種方式是鑒于對(duì)純?cè)玩準(zhǔn)嚼^承缺陷的改進(jìn),來(lái)進(jìn)一步提升繼承的實(shí)用性。思路即在子類(lèi)構(gòu)造函數(shù)的內(nèi)部調(diào)用超類(lèi)構(gòu)造函數(shù)。什么意思?
? 函數(shù)只不過(guò)是在特定的環(huán)境中執(zhí)行代碼的對(duì)象,且它具有私有作用域。當(dāng)我們將超類(lèi)在子類(lèi)內(nèi)部調(diào)用時(shí),就將超類(lèi)的作用域限定在了子類(lèi)內(nèi)部,是一種“借調(diào)”的思想,超類(lèi)屬性就變成了子類(lèi)的實(shí)例屬性,從而實(shí)現(xiàn)子類(lèi)不同實(shí)例對(duì)象所繼承來(lái)的屬性互補(bǔ)干擾的效果。
? 涉及到函數(shù)執(zhí)行環(huán)境改變,很容易想到使用apply()call()來(lái)實(shí)現(xiàn)。贅述一下applycall第一個(gè)參數(shù)都是thisapply其他參數(shù)封裝在一個(gè)數(shù)組里,call則直接列出即可。

function Super() {
  this.colors = ['red', 'blue']
}
function Sub() {
  Super.call(this)
}

var instance1 = new Sub()
instance1.colors.push('green')
console.log(instance1.colors) // "red, blue, green"

var instance2 = new Sub()
instance2.colors.push('yellow')
console.log(instance2.colors) // "red, blue, yellow"

? 從兩個(gè)打印結(jié)果也可以看出,子類(lèi)實(shí)例已經(jīng)實(shí)現(xiàn)了數(shù)據(jù)隔離,另外,因?yàn)槌?lèi)會(huì)在子類(lèi)內(nèi)部調(diào)用,因此,但實(shí)例化的時(shí)候給子類(lèi)傳遞參數(shù),也能夠被超類(lèi)所接手,如下

function Sub(c) {
  Super.call(this, c)
}
var instance3 = new Sub('pink')

【缺陷】
(1)由于超類(lèi)中的所有屬性都會(huì)轉(zhuǎn)成子類(lèi)的實(shí)例屬性,包括方法,導(dǎo)致了方法無(wú)法復(fù)用
(2)在超類(lèi)的原型中定義的方法對(duì)子類(lèi)型不可見(jiàn),結(jié)果導(dǎo)致只能使用構(gòu)造函數(shù)定義方法,還是不能復(fù)用方法
?

# 組合繼承(最常用)

? 結(jié)合以上兩種繼承的優(yōu)點(diǎn),其思路是 使用原型鏈實(shí)現(xiàn)對(duì)原型屬性和方法的繼承,而通過(guò)借用構(gòu)造函數(shù)來(lái)實(shí)現(xiàn)對(duì)屬性的繼承。以達(dá)到方法能夠復(fù)用,屬性能夠隔離的目的。

function Super(name) {
  this.name = name
  this.colors = ['red', 'blue']
}
Super.prototype.sayName = function() {
  console.log(this.name)
}
function Sub(name) {
  Super.call(this, name) // 第二次調(diào)用超類(lèi)型
  this.sex = 'male'
}

Sub.prototype = new Super()  // 第一次調(diào)用超類(lèi)型
Sub.prototype.constructor = Sub // 修正構(gòu)造器丟失問(wèn)題

var instance1 = new Sub('zfs')
instance1.colors.push('green')
console.log(instance1.colors) // "red, blue, green"
instance1.sayName() // 'zfs'

var instance2 = new Sub()
instance2.colors.push('wmm')
console.log(instance2.colors) // "red, blue, yellow"
instance2.sayName() // 'wmm'

? 細(xì)心的你可能會(huì)疑惑,這里使用了原型鏈繼承,是否依舊會(huì)有超類(lèi)實(shí)例屬性共享的問(wèn)題。答案是有的,它們依舊正常存在于子類(lèi)的原型屬性中,當(dāng)時(shí)因?yàn)樽宇?lèi)內(nèi)部實(shí)現(xiàn)了對(duì)超類(lèi)屬性的“借調(diào)”,子類(lèi)實(shí)例屬性中也有一份超類(lèi)實(shí)例屬性的屬性備份,因此覆蓋了原型屬性中的那部分,從而子類(lèi)實(shí)例對(duì)象中實(shí)例屬性并不會(huì)互相影響。
【缺陷】
(1)正如以上所說(shuō),在子類(lèi)的實(shí)例中其實(shí)是有兩份來(lái)自超類(lèi)的實(shí)例屬性的,一份來(lái)自于原型對(duì)象中的原型屬性,一份來(lái)自于“借調(diào)”超類(lèi)而來(lái)的實(shí)例屬性,也就是說(shuō),他調(diào)用了兩次超類(lèi)型構(gòu)造函數(shù)。
?

# 寄生組合式繼承

? 說(shuō)這個(gè)繼承方式之前,必須先理解Object.create()的用法

* Object.create()

? Object.create(proto[, propertiesObject])方法用于在現(xiàn)有提供的原型對(duì)象下創(chuàng)建一個(gè)新的對(duì)象,接受兩個(gè)參數(shù):
(1)proto: 必須。表示新建對(duì)象的原型對(duì)象,該參數(shù)會(huì)復(fù)制到目標(biāo)對(duì)象的原型上??梢允?code>null,普通對(duì)象,函數(shù)的prototype屬性
(2)propertiesObject: 可選。添加到新創(chuàng)建對(duì)象的可枚舉屬性,這些屬性會(huì)被添加為實(shí)例屬性。

對(duì)比 new Object()Object.create()的區(qū)別

var proto = { like: 'apple' }
var inst1 = new Object(proto)
console.log(inst1) // { like: 'apple' }
console.log(inst1.__proto__) // {}

var inst2 = Object.create(proto)
console.log(inst2) // {}
console.log(inst2.__proto__) // { like: 'apple' }

?
? 回到寄生式組合,它的思路是創(chuàng)建一個(gè)僅用于封裝繼承過(guò)程的函數(shù),該函數(shù)在內(nèi)部以某種方式來(lái)增強(qiáng)對(duì)象,最后再像真的是它做了所有工作一樣返回對(duì)象。

function createAnother(origin) {
  var clone = Object.create(origin) // 根據(jù)入?yún)?duì)象創(chuàng)建新對(duì)象,將入?yún)?duì)象屬性掛到原型上
  clone.sayHi = function() {  // 給對(duì)象創(chuàng)建一個(gè)實(shí)例方法來(lái)增強(qiáng)對(duì)象
    console.log('hello')
  }
  return clone;  // 返回對(duì)象
}

? 以上代碼的執(zhí)行思路就是,把“超類(lèi)”對(duì)象 origin傳遞給一個(gè)工廠函數(shù)createAnother(),在工廠函數(shù)中,利用Object.create()把超類(lèi)屬性都掛到新對(duì)象的原型上,再創(chuàng)建一些實(shí)例屬性來(lái)增強(qiáng)對(duì)象,最后將其輸出。這樣由該工廠函數(shù)返回的對(duì)象中就可以實(shí)現(xiàn)我們所需要的實(shí)例屬性與原型屬性。使用createAnother()函數(shù)方法如下:

var super = {
  name: 'zfs',
  colors: ['red', 'blue']
}
var person1 = createAnother(super)
person1.sayHi() // 'hello'

【缺陷】創(chuàng)建的實(shí)例方法不可復(fù)用,與構(gòu)造函數(shù)模式類(lèi)似
?

# 寄生組合式繼承(最理想)

? 組合繼承是最常用的繼承方式,不過(guò)它也有自己的不足,即無(wú)論什么情況下,都會(huì)調(diào)用兩次超類(lèi)構(gòu)造函數(shù):第一次是在創(chuàng)建子類(lèi)型原型的時(shí)候,第二次是在子類(lèi)型構(gòu)造函數(shù)內(nèi)部。也就是說(shuō)子類(lèi)型最終會(huì)包含超類(lèi)型對(duì)象的全部實(shí)例屬性在原型對(duì)象中,但又不得不調(diào)用子類(lèi)型構(gòu)造函數(shù)來(lái)重寫(xiě)這些屬性。
? 寄生組合式繼承通過(guò)借用構(gòu)造函數(shù)來(lái)實(shí)現(xiàn)繼承屬性,通過(guò)原型鏈的混成形式來(lái)繼承方法,基本思路是:不必為了指定子類(lèi)型的原型而調(diào)用超類(lèi)型的構(gòu)造函數(shù),需要的無(wú)非就是超類(lèi)原型的一個(gè)副本而已,實(shí)例屬性并不需要,因此我們可以構(gòu)造如下超類(lèi)實(shí)例化的步驟。

function inheritPrototype(sub, super) {
  var prototype = Object.create(super.prototype)  // 創(chuàng)建對(duì)象
  prototype.constructor = sub  // 增強(qiáng)對(duì)象
  sub.prototype = prototype  // 指定對(duì)象
}

? 以上代碼中,第一步是創(chuàng)建超類(lèi)型原型的副本,第二步是為創(chuàng)建的副本增加constructor從而彌補(bǔ)因重寫(xiě)原型而失去的默認(rèn)屬性。第三步,將創(chuàng)建的對(duì)象賦值給子類(lèi)的原型。這樣,我們就可以調(diào)用inheritPrototype()函數(shù)的語(yǔ)句,去替換重寫(xiě)子類(lèi)原型為超類(lèi)實(shí)例的語(yǔ)句了。用法如下:

function Super(name) {
  this.name = name
  this.colors = ['red', 'blue']
}
Super.prototype.sayName = function() {
  console.log(this.name)
}
function Sub(name, age) {
  Super.call(this, name)
  this.age = age
}
inheritPrototype(Sub, Super)
Sub.prototype.sayAge = function() {
  console.log(this.age)
}
var ps1= new Sub('zfs', 25)
var ps2= new Sub('wmm', 18)
ps1.colors.push('pink')
console.log(ps1.colors) // ['red', 'blue', 'pink']
console.log(ps2.colors) // ['red', 'blue']

其原型關(guān)系如下所示


? 這種模式的高效率體現(xiàn)在它只調(diào)用了一次 Super構(gòu)造函數(shù),避免了建立多余屬性,于此同時(shí)還能保持原型鏈不變,因此能夠正常使用instanceofisPrototypeOf()。是最理想的繼承方式。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容