? 繼承是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)型 Super 和 Sub,后者重寫(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)。贅述一下apply和call第一個(gè)參數(shù)都是this,apply其他參數(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í)還能保持原型鏈不變,因此能夠正常使用instanceof和isPrototypeOf()。是最理想的繼承方式。