內(nèi)容承接 面向?qū)ο螅ǘ?/a>
繼承
ECMAScript中描述了原型鏈的概念,并將原型鏈作為實(shí)現(xiàn)繼承的主要方法。其基本思想是利用原型讓一個(gè)引用類型繼承另一個(gè)引用類型的屬性和方法。
原型鏈的基本概念
簡單回顧一下構(gòu)造函數(shù)、原型和實(shí)例的關(guān)系:每個(gè)構(gòu)造函數(shù)都有一個(gè)原型對(duì)象,原型對(duì)象都包含一個(gè)指向構(gòu)造函數(shù)的指針,而實(shí)例都包含一個(gè)指向原型對(duì)象的內(nèi)部指針。
那么,假如我們讓原型對(duì)象等于另一個(gè)類型的實(shí)例,結(jié)果會(huì)怎么樣呢?顯然,此時(shí)的原型對(duì)象將包含一個(gè)指向另一個(gè)原型的指針,相應(yīng)地,另一個(gè)原型中也包含著一個(gè)指向另一個(gè)構(gòu)造函數(shù)的指針。假如另一個(gè)原型又是另一個(gè)類型的實(shí)例,那么上述關(guān)系依然成立,如此層層遞進(jìn),就構(gòu)成了實(shí)例與原型的鏈條。這就是所謂原型鏈的基本概念。
function SuperType() {
this.property = true
}
SuperType.prototype.getSuperValue = function() {
return this.property
}
function SubType() {
this.subproperty = false
}
//繼承了 SuperType
SubType.prototype = new SuperType()
SubType.prototype.getSubValue = function () {
return this.subproperty
}
const instance = new SubType()
alert(instance.getSuperValue()) //true
以上代碼定義了兩個(gè)類型: SuperType和SubType。每個(gè)類型分別有一個(gè)屬性和一個(gè)方法。它們的主要區(qū)別是SubType 繼承了 SuperType ,而繼承是通過創(chuàng)建SuperType的實(shí)例,并將該實(shí)例賦給SubType.prototype實(shí)現(xiàn)的。
實(shí)現(xiàn)繼承的本質(zhì)是重寫原型對(duì)象,代之以一個(gè)新類型的實(shí)例。
原型搜索機(jī)制
當(dāng)以讀取模式訪問一個(gè)實(shí)例屬性時(shí),首先會(huì)在實(shí)例中搜索該屬性。如果沒有找到該屬性,則會(huì)繼續(xù)搜索實(shí)例的原型。在通過原型鏈實(shí)現(xiàn)繼承的情況下,搜索過程就得以沿著原型鏈繼續(xù)向上。就拿上面的例子來說,調(diào)用instance.getSuperValue() 會(huì)經(jīng)歷三個(gè)搜索步驟:
- 搜索實(shí)例;
- 搜索
SubType.prototype; - 搜索
SuperType.prototype,最后一步才會(huì)找到該方法。在找不到屬性或方法的情況下,搜索過程總是要一環(huán)一環(huán)地前行到原型鏈末端才會(huì)停下來。
細(xì)節(jié)一:別忘記默認(rèn)的原型
所有引用類型默認(rèn)都繼承了Object,而這個(gè)繼承也是通過原型鏈實(shí)現(xiàn)的。大家要記住,所有函數(shù)的默認(rèn)原型都是Object的實(shí)例,因此默認(rèn)原型都會(huì)包含一個(gè)內(nèi)部指針,指Object.prototype。
如何確定原型和實(shí)例的關(guān)系
1. instanceof
可以通過兩種方式來確定原型和實(shí)例之間的關(guān)系。第一種方式是使用instanceof操作符,只要用這個(gè)操作符來測試實(shí)例與原型鏈中出現(xiàn)過的構(gòu)造函數(shù),結(jié)果就會(huì)返回true。以下幾行代碼就說明了這一點(diǎn)。
alert(instance instanceof Object) //true
alert(instance instanceof SuperType) //true
alert(instance instanceof SubType) //true
2. isPrototypeOf()
第二種方式是使用 isPrototypeOf()方法。同樣,只要是原型鏈中出現(xiàn)過的原型,都可以說是該原型鏈所派生的實(shí)例的原型,因此isPrototypeOf()方法也會(huì)返回 true,如下所示。
alert(Object.prototype.isPrototypeOf(instance)) //true
alert(SuperType.prototype.isPrototypeOf(instance)) //true
alert(SubType.prototype.isPrototypeOf(instance)) //true
細(xì)節(jié)二:謹(jǐn)慎地定義方法
子類型有時(shí)候需要重寫超類型中的某個(gè)方法,或者需要添加超類型中不存在的某個(gè)方法。但不管怎樣,給原型添加方法的代碼一定要放在替換原型的語句之后。來看下面的例子。
function SuperType() {
this.property = true
}
SuperType.prototype.getSuperValue = function () {
return this.property
}
function SubType() {
this.subproperty = false
}
//繼承了 SuperType
SubType.prototype = new SuperType()
// 添加新方法
SubType.prototype.getSubValue = function () {
return this.subproperty
}
// 重寫超類型中的方法
SubType.prototype.getSuperValue = function () {
return false
}
const instance = new SubType()
alert(instance.getSuperValue()) //false
第一個(gè)方法 getSubValue()被添加到了SubType中。第二個(gè)方法 getSuperValue()是原型鏈中已經(jīng)存在的一個(gè)方法,但重寫這個(gè)方法將會(huì)屏蔽原來的那個(gè)方法。換句話說,當(dāng)通過SubType 的實(shí)例調(diào)用getSuperValue()時(shí),調(diào)用的就是這個(gè)重新定義的方法;但通過SuperType的實(shí)例調(diào)用 getSuperValue()時(shí),還會(huì)繼續(xù)調(diào)用原來的那個(gè)方法。
這里要格外注意的是,必須在用
SuperType的實(shí)例替換原型之后,再定義這兩個(gè)方法。
細(xì)節(jié)三:不使用對(duì)象字面量創(chuàng)建原型方法
在通過原型鏈實(shí)現(xiàn)繼承時(shí),不能使用對(duì)象字面量創(chuàng)建原型方法。因?yàn)檫@樣做就會(huì)重寫原型鏈,如下面的例子所示。
function SuperType() {
this.property = true
}
SuperType.prototype.getSuperValue = function () {
return this.property
}
function SubType() {
this.subproperty = false
}
//繼承了 SuperType
SubType.prototype = new SuperType()
// 使用字面量添加新方法,會(huì)導(dǎo)致上一行代碼無效
SubType.prototype = {
getSubValue: function () {
return this.subproperty
},
someOtherMethod: function () {
return false;
}
}
const instance = new SubType()
alert(instance.getSuperValue()) //error!
原型鏈的問題
在通過原型來實(shí)現(xiàn)繼承時(shí),原型實(shí)際上會(huì)變成另一個(gè)類型的實(shí)例。于是,原先的實(shí)例屬性也就順理成章地變成了現(xiàn)在的原型屬性了。
function SuperType() {
this.colors = ["red", "blue", "green"]
}
function SubType() {
}
//繼承了 SuperType
SubType.prototype = new SuperType()
const instance1 = new SubType()
instance1.colors.push("black")
alert(instance1.colors) // red,blue,green,black"
const instance2 = new SubType()
alert(instance2.colors) // "red,blue,green,black"
當(dāng) SubType通過原型鏈繼承了SuperType 之后,SubType.prototype就變成了SuperType的一個(gè)實(shí)例,因此它也擁有了一個(gè)它自己的colors屬性——就跟專門創(chuàng)建了一個(gè) SubType.prototype.colors 屬性一樣。但結(jié)果是什么呢?
結(jié)果是SubType的所有實(shí)例都會(huì)共享這一個(gè)colors 屬性。而我們對(duì) instance1.colors的修改能夠通過instance2.colors反映出來,就已經(jīng)充分證實(shí)了這一點(diǎn)。
原型鏈的第二個(gè)問題是:在創(chuàng)建子類型的實(shí)例時(shí),不能向超類型的構(gòu)造函數(shù)中傳遞參數(shù)。實(shí)際上,應(yīng)該說是沒有辦法在不影響所有對(duì)象實(shí)例的情況下,給超類型的構(gòu)造函數(shù)傳遞參數(shù)。有鑒于此,再加上前面剛剛討論過的由于原型中包含引用類型值所帶來的問題,實(shí)踐中很少會(huì)單獨(dú)使用原型鏈。
實(shí)現(xiàn)繼承的方式
方式一:借用構(gòu)造函數(shù)
在解決原型中包含引用類型值所帶來問題的過程中,開發(fā)人員開始使用一種叫做借用構(gòu)造函數(shù)(constructor stealing)的技術(shù)(有時(shí)候也叫做偽造對(duì)象或經(jīng)典繼承)。即在子類型構(gòu)造函數(shù)的內(nèi)部調(diào)用超類型構(gòu)造函數(shù)。別忘了,函數(shù)只不過是在特定環(huán)境中執(zhí)行代碼的對(duì)象,因此通過使用 apply()和 call()方法也可以在(將來)新創(chuàng)建的對(duì)象上執(zhí)行構(gòu)造函數(shù),如下所示:
function SuperType() {
this.colors = ["red", "blue", "green"]
}
function SubType() {
// 繼承了 SuperType
SuperType.call(this)
}
const instance1 = new SubType()
instance1.colors.push("black")
alert(instance1.colors) // "red,blue,green,black"
const instance2 = new SubType()
alert(instance2.colors) // "red,blue,green"
通過使用call()方法(或 apply()方法也可以),我們實(shí)際上是在(未來將要)新創(chuàng)建的SubType實(shí)例的環(huán)境下調(diào)用了 SuperType構(gòu)造函數(shù)。這樣一來,就會(huì)在新 SubType對(duì)象上執(zhí)行SuperType() 函數(shù)中定義的所有對(duì)象初始化代碼。
優(yōu)點(diǎn): 傳遞參數(shù)
相對(duì)于原型鏈而言,借用構(gòu)造函數(shù)有一個(gè)很大的優(yōu)勢,即可以在子類型構(gòu)造函數(shù)中向超類型構(gòu)造函數(shù)傳遞參數(shù)??聪旅孢@個(gè)例子。
function SuperType(name) {
this.name = name
}
function SubType() {
//繼承了 SuperType,同時(shí)還傳遞了參數(shù)
SuperType.call(this, "Nicholas")
//實(shí)例屬性
this.age = 29
}
const instance = new SubType()
alert(instance.name) //"Nicholas";
alert(instance.age) //29
缺點(diǎn):函數(shù)復(fù)用
如果僅僅是借用構(gòu)造函數(shù),那么也將無法避免構(gòu)造函數(shù)模式存在的問題——方法都在構(gòu)造函數(shù)中定義,因此函數(shù)復(fù)用就無從談起了。而且,在超類型的原型中定義的方法,對(duì)子類型而言也是不可見的,結(jié)果所有類型都只能使用構(gòu)造函數(shù)模式??紤]到這些問題,借用構(gòu)造函數(shù)的技術(shù)也是很少單獨(dú)使用的。
方式二:組合繼承
組合繼承(combination inheritance),有時(shí)候也叫做偽經(jīng)典繼承,指的是將原型鏈和借用構(gòu)造函數(shù)的技術(shù)組合到一塊,從而發(fā)揮二者之長的一種繼承模式。
其背后的思路是使用原型鏈實(shí)現(xiàn)對(duì)原型屬性和方法的繼承,而通過借用構(gòu)造函數(shù)來實(shí)現(xiàn)對(duì)實(shí)例屬性的繼承。這樣,既通過在原型上定義方法實(shí)現(xiàn)了函數(shù)復(fù)用,又能夠保證每個(gè)實(shí)例都有它自己的屬性。
function SuperType(name) {
this.name = name
this.colors = ["red", "blue", "green"]
}
SuperType.prototype.sayName = function () {
alert(this.name)
}
function SubType(name, age) {
//繼承屬性
SuperType.call(this, name)
this.age = age
}
//繼承方法
SubType.prototype = new SuperType()
SubType.prototype.constructor = SubType
SubType.prototype.sayAge = function () {
alert(this.age)
}
const instance1 = new SubType("Nicholas", 29)
instance1.colors.push("black")
alert(instance1.colors) //"red,blue,green,black"
instance1.sayName(); //"Nicholas";
instance1.sayAge(); //29
const instance2 = new SubType("Greg", 27)
alert(instance2.colors) //"red,blue,green"
instance2.sayName() //"Greg";
instance2.sayAge() //27
SubType 構(gòu)造函數(shù)在調(diào)用SuperType構(gòu)造函數(shù)時(shí)傳入了 name 參數(shù),緊接著又定義了它自己的屬性age。然后,將SuperType的實(shí)例賦值給 SubType的原型,然后又在該新原型上定義了方法sayAge()。這樣一來,就可以讓兩個(gè)不同的 SubType 實(shí)例既分別擁有自己屬性——包括colors屬性,又可以使用相同的方法了。
組合繼承避免了原型鏈和借用構(gòu)造函數(shù)的缺陷,融合了它們的優(yōu)點(diǎn),成為
JavaScript中最常用的繼承模式。而且,instanceof和isPrototypeOf()也能夠用于識(shí)別基于組合繼承創(chuàng)建的對(duì)象。
方式三:原型式繼承
道格拉斯·克羅克福德在 2006年寫了一篇文章,題為Prototypal Inheritance in JavaScript(JavaScript中的原型式繼承)。在這篇文章中,他介紹了一種實(shí)現(xiàn)繼承的方法,這種方法并沒有使用嚴(yán)格意義上的構(gòu)造函數(shù)。他的想法是借助原型可以基于已有的對(duì)象創(chuàng)建新對(duì)象,同時(shí)還不必因此創(chuàng)建自定義類型。為了達(dá)到這個(gè)目的,他給出了如下函數(shù)。
function object(o) {
function F() {
}
F.prototype = o
return new F()
}
在object() 函數(shù)內(nèi)部,先創(chuàng)建了一個(gè)臨時(shí)性的構(gòu)造函數(shù),然后將傳入的對(duì)象作為這個(gè)構(gòu)造函數(shù)的原型,最后返回了這個(gè)臨時(shí)類型的一個(gè)新實(shí)例。
從本質(zhì)上講,
object()對(duì)傳入其中的對(duì)象執(zhí)行了一次淺復(fù)制。
function object(o) {
function F() {
}
F.prototype = o
return new F()
}
const person = {
name: "Nicholas",
friends: ["Shelby", "Court", "Van"]
}
const anotherPerson = object(person)
anotherPerson.name = "Greg"
anotherPerson.friends.push("Rob")
const yetAnotherPerson = object(person)
yetAnotherPerson.name = "Linda"
yetAnotherPerson.friends.push("Barbie")
alert(person.friends) //"Shelby,Court,Van,Rob,Barbie"
克羅克福德主張的這種原型式繼承,要求你必須有一個(gè)對(duì)象可以作為另一個(gè)對(duì)象的基礎(chǔ)。如果有這么一個(gè)對(duì)象的話,可以把它傳遞給object()函數(shù),然后再根據(jù)具體需求對(duì)得到的對(duì)象加以修改即可。
可以作為另一個(gè)對(duì)象基礎(chǔ)的是person對(duì)象,于是我們把它傳入到object()函數(shù)中,然后該函數(shù)就會(huì)返回一個(gè)新對(duì)象。這個(gè)新對(duì)象將person作為原型,所以它的原型中就包含一個(gè)基本類型值屬性和一個(gè)引用類型值屬性。這意味著person.friends不僅屬于person所有,而且也會(huì)被anotherPerson以及yetAnotherPerson 共享。
Object.create()
ECMAScript 5通過新增 Object.create()方法規(guī)范化了原型式繼承。這個(gè)方法接收兩個(gè)參數(shù):一個(gè)用作新對(duì)象原型的對(duì)象和(可選的)一個(gè)為新對(duì)象定義額外屬性的對(duì)象。在傳入一個(gè)參數(shù)的情況下,Object.create() 與 object()方法的行為相同。
const person = {
name: "Nicholas",
friends: ["Shelby", "Court", "Van"]
}
const anotherPerson = Object.create(person)
anotherPerson.name = "Greg"
anotherPerson.friends.push("Rob")
const yetAnotherPerson = Object.create(person)
yetAnotherPerson.name = "Linda"
yetAnotherPerson.friends.push("Barbie")
alert(person.friends) //"Shelby,Court,Van,Rob,Barbie"
Object.create()方法的第二個(gè)參數(shù)與Object.defineProperties()方法的第二個(gè)參數(shù)格式相同:每個(gè)屬性都是通過自己的描述符定義的。以這種方式指定的任何屬性都會(huì)覆蓋原型對(duì)象上的同名屬性。
const person = {
name: "Nicholas",
friends: ["Shelby", "Court", "Van"]
}
const anotherPerson = Object.create(person, {
name: {
value: "Greg"
}
})
alert(anotherPerson.name) //"Greg"
支持Object.create()方法的瀏覽器有IE9+、Firefox 4+、Safari 5+、Opera 12+和 Chrome。
方式四:寄生式繼承
寄生式繼承的思路與寄生構(gòu)造函數(shù)和工廠模式類似,即創(chuàng)建一個(gè)僅用于封裝繼承過程的函數(shù),該函數(shù)在內(nèi)部以某種方式來增強(qiáng)對(duì)象,最后再像真地是它做了所有工作一樣返回對(duì)象。以下代碼示范了寄生式繼承模式。
function createAnother(original) {
const clone = object(original) //通過調(diào)用函數(shù)創(chuàng)建一個(gè)新對(duì)象
clone.sayHi = function () { //以某種方式來增強(qiáng)這個(gè)對(duì)象
alert("hi")
}
return clone //返回這個(gè)對(duì)象
}
在這個(gè)例子中,createAnother() 函數(shù)接收了一個(gè)參數(shù),也就是將要作為新對(duì)象基礎(chǔ)的對(duì)象。然后,把這個(gè)對(duì)象(original)傳遞給object()函數(shù),將返回的結(jié)果賦值給clone 。再為 clone對(duì)象添加一個(gè)新方法sayHi(),最后返回clone對(duì)象??梢韵裣旅孢@樣來使用 createAnother() 函數(shù):
function createAnother(original) {
const clone = object(original) //通過調(diào)用函數(shù)創(chuàng)建一個(gè)新對(duì)象
clone.sayHi = function () { //以某種方式來增強(qiáng)這個(gè)對(duì)象
alert("hi")
}
return clone //返回這個(gè)對(duì)象
}
const person = {
name: "Nicholas",
friends: ["Shelby", "Court", "Van"]
}
const anotherPerson = createAnother(person)
anotherPerson.sayHi() //"hi"
這個(gè)例子中的代碼基于person 返回了一個(gè)新對(duì)象—— anotherPerson。新對(duì)象不僅具有person的所有屬性和方法,而且還有自己的 sayHi()方法。
使用寄生式繼承來為對(duì)象添加函數(shù),會(huì)由于不能做到函數(shù)復(fù)用而降低效率;這一點(diǎn)與構(gòu)造函數(shù)模式類似。
方式五:寄生組合式繼承
組合繼承是JavaScript最常用的繼承模式;不過,它也有自己的不足。組合繼承最大的問題就是無論什么情況下,都會(huì)調(diào)用兩次超類型構(gòu)造函數(shù):一次是在創(chuàng)建子類型原型的時(shí)候,另一次是在子類型構(gòu)造函數(shù)內(nèi)部。
function SuperType(name) {
this.name = name
this.colors = ["red", "blue", "green"]
}
SuperType.prototype.sayName = function () {
alert(this.name)
};
function SubType(name, age) {
SuperType.call(this, name) // 第二次調(diào)用 SuperType()
this.age = age
}
SubType.prototype = new SuperType() // 第一次調(diào)用 SuperType()
SubType.prototype.constructor = SubType
SubType.prototype.sayAge = function () {
alert(this.age)
}
在第一次調(diào)用SuperType 構(gòu)造函數(shù)時(shí),SubType.prototype會(huì)得到兩個(gè)屬性:name和colors;它們都是SuperType 的實(shí)例屬性,只不過現(xiàn)在位于SubType的原型中。當(dāng)調(diào)用 SubType構(gòu)造函數(shù)時(shí),又會(huì)調(diào)用一次SuperType 構(gòu)造函數(shù),這一次又在新對(duì)象上創(chuàng)建了實(shí)例屬性name和colors 。于是,這兩個(gè)屬性就屏蔽了原型中的兩個(gè)同名屬性。
所謂寄生組合式繼承,即通過借用構(gòu)造函數(shù)來繼承屬性,通過原型鏈的混成形式來繼承方法。其背后的基本思路是:不必為了指定子類型的原型而調(diào)用超類型的構(gòu)造函數(shù),我們所需要的無非就是超類型原型的一個(gè)副本而已。本質(zhì)上,就是使用寄生式繼承來繼承超類型的原型,然后再將結(jié)果指定給子類型的原型。寄生組合式繼承的基本模式如下所示。
function inheritPrototype(subType, superType){
const prototype = object(superType.prototype) //創(chuàng)建對(duì)象
prototype.constructor = subType //增強(qiáng)對(duì)象
subType.prototype = prototype //指定對(duì)象
}
這個(gè)示例中的 inheritPrototype()函數(shù)實(shí)現(xiàn)了寄生組合式繼承的最簡單形式。這個(gè)函數(shù)接收兩個(gè)參數(shù):子類型構(gòu)造函數(shù)和超類型構(gòu)造函數(shù)。
在函數(shù)內(nèi)部,第一步是創(chuàng)建超類型原型的一個(gè)副本。第二步是為創(chuàng)建的副本添加 constructor 屬性,從而彌補(bǔ)因重寫原型而失去的默認(rèn)的constructor 屬性。最后一步,將新創(chuàng)建的對(duì)象(即副本)賦值給子類型的原型。這樣,我們就可以用調(diào)用inherit-Prototype()函數(shù)的語句,去替換前面例子中為子類型原型賦值的語句了,例如:
function inheritPrototype(subType, superType) {
const prototype = object(superType.prototype) //創(chuàng)建對(duì)象
prototype.constructor = subType //增強(qiáng)對(duì)象
subType.prototype = prototype //指定對(duì)象
}
function SuperType(name) {
this.name = name;
this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function () {
alert(this.name)
}
function SubType(name, age) {
SuperType.call(this, name)
this.age = age
}
inheritPrototype(SubType, SuperType)
SubType.prototype.sayAge = function () {
alert(this.age)
}
這個(gè)例子的高效率體現(xiàn)在它只調(diào)用了一次 SuperType 構(gòu)造函數(shù),并且因此避免了在 SubType.prototype上面創(chuàng)建不必要的、多余的屬性。與此同時(shí),原型鏈還能保持不變;因此,還能夠正常使用instanceof 和 isPrototypeOf()。開發(fā)人員普遍認(rèn)為寄生組合式繼承是引用類型最理想的繼承范式。