前面, 我們討論了很多種場景對(duì)象的方式: 從Object到字面量, 再到工廠模式, 再到構(gòu)造函數(shù).
最終我們發(fā)現(xiàn), 構(gòu)造函數(shù)是比較理想的一種方式, 但是它也存在問題.
為了最終解決這個(gè)問題, 我們需要學(xué)習(xí)一個(gè)新的知識(shí): 原型(prototype).
一. 理解原型模式
1.1. 什么是原型呢?
你需要先知道一個(gè)事實(shí):
我們創(chuàng)建的每個(gè)函數(shù)都有一個(gè)prototype(原型)屬性
這個(gè)屬性是一個(gè)指針蜂挪,指向一個(gè)對(duì)象
而這個(gè)對(duì)象的作用是存放這個(gè)類型創(chuàng)建的所有實(shí)例共享的屬性和方法谱秽。
指向的這個(gè)對(duì)象, 就是我們的所謂的原型對(duì)象.
原型對(duì)象的作用:
使用原型對(duì)象的好處是可以讓所有對(duì)象實(shí)例共享它所包含的屬性和方法。
換句話說,不必在構(gòu)造函數(shù)中定義對(duì)象實(shí)例的信息篇梭,而是可以將這些信息直接添加到原型對(duì)象中。
我們來看看原型對(duì)象的使用:
//?創(chuàng)建對(duì)象的構(gòu)造函數(shù)
functionPerson(){}
//?通過原型對(duì)象來設(shè)置一些屬性和值
Person.prototype.name?="Coderwhy"
Person.prototype.age?=18
Person.prototype.height?=1.88
Person.prototype.sayHello?=function(){
alert(this.name)
}
//?創(chuàng)建兩個(gè)對(duì)象,?并且調(diào)用方法
varperson1?=newPerson()
varperson2?=newPerson()
person1.sayHello()//?Coderwhy
person2.sayHello()//?Coderwhy
代碼解析:
在上面的代碼中, 我們沒有給實(shí)例對(duì)象單獨(dú)設(shè)置屬性和方法, 而是直接設(shè)置給了原型對(duì)象.
而原型對(duì)象的作用是可以讓所有的對(duì)象來共享這些屬性和方法.
因此, 我們調(diào)用sayHello()方法時(shí), 它們打印的結(jié)果是一樣的, 它們是共享的.
1.2. 深入原型對(duì)象
原型對(duì)象的創(chuàng)建:
無論什么時(shí)候泛粹,只要?jiǎng)?chuàng)建了一個(gè)新函數(shù)埃儿,就會(huì)根據(jù)一組特定的規(guī)則為該函數(shù)創(chuàng)建一個(gè)prototype屬性,這個(gè)屬性指向函數(shù)的原型對(duì)象掩宜。
原型上的constructor屬性:
默認(rèn)情況下蔫骂,所有原型對(duì)象都會(huì)自動(dòng)獲得一個(gè)constructor(構(gòu)造函數(shù))屬性,這個(gè)屬性包含一個(gè)指向prototype屬性所在函數(shù)的指針牺汤。
用我們上面的例子來說, Person.prototype.constructor指向Person辽旋。
也就是原型對(duì)象自身來說, 只有一個(gè)constructor屬性, 而其他屬性可以由我們添加或者從Object中繼承.
新的實(shí)例創(chuàng)建時(shí), 原型對(duì)象在哪里呢?
當(dāng)調(diào)用構(gòu)造函數(shù)創(chuàng)建一個(gè)新實(shí)例后,該實(shí)例的內(nèi)部將包含一個(gè)內(nèi)部屬性,該屬性的指針, 指向構(gòu)造函數(shù)的原型對(duì)象补胚。
這個(gè)屬性是_proto_
簡單說, 每個(gè)實(shí)例中, 其實(shí)也會(huì)有一個(gè)屬性, 該屬性是指向原型對(duì)象的.
//?原型對(duì)象中有一個(gè)屬性:?constructor屬性
//?屬性指向Person函數(shù)
console.log(Person.prototype.constructor);//?Person函數(shù)
//?對(duì)象實(shí)例也有一個(gè)屬性指向原型
console.log(person1.__proto__);//?原型對(duì)象
console.log(Person.prototype);//?原型對(duì)象
console.log(person1.__proto__?===?Person.prototype);//?true
我們通過一個(gè)圖來解釋上面的概念:
img
解析:
上面的圖解析了Person構(gòu)造函數(shù)码耐、Person的原型屬性以及Person現(xiàn)有的兩個(gè)實(shí)例之間的關(guān)系
Person.prototype指向原型對(duì)象, 而Person.prototype.constructor又指回了Person.
原型對(duì)象中除了包含constructor屬性之外,還包括后來添加的其他屬性溶其。
Person的每個(gè)實(shí)例——personl和person2都包含一個(gè)內(nèi)部屬性_proto_骚腥,該屬性也指向原型對(duì)象;
對(duì)象搜索屬性和方法的過程:
每當(dāng)代碼讀取某個(gè)對(duì)象的某個(gè)屬性時(shí)瓶逃,都會(huì)執(zhí)行一次搜索束铭,也就是要找到給定名稱的屬性。
搜索首先從對(duì)象實(shí)例本身開始
如果在實(shí)例中找到了具有給定名字的屬性厢绝,則返回該屬性的值契沫;
如果沒有找到,則繼續(xù)搜索指針指向的原型對(duì)象昔汉,在原型對(duì)象中查找具有給定名字的屬性
如果在原型對(duì)象中找到了這個(gè)屬性懈万,則返回該屬性的值。
也就是說靶病,在我們調(diào)用personl.sayHello()的時(shí)候会通,會(huì)先后執(zhí)行兩次搜索。
現(xiàn)在我們也能理解, 為什么所有的實(shí)例中都包含一個(gè)constructor屬性, 這是因?yàn)槟J(rèn)所有的原型對(duì)象中都包含了該屬性.
可以通過__proto__來修改原型的值(通常不會(huì)這樣修改, 知道即可)
你可以理解為什么person1修改了name后, person2也會(huì)修改嗎?
通過上面的圖, 自己再來理解一下吧.
person1.sayHello()//?Coderwhy
person2.sayHello()//?Coderwhy
person1.__proto__.name?="Kobe"
person1.sayHello()//?Kobe
person2.sayHello()//?Kobe
但是要注意下面的情況:
當(dāng)我們給person1.name進(jìn)行賦值時(shí), 其實(shí)在給person1實(shí)例添加一個(gè)name屬性.
這個(gè)時(shí)候再次訪問時(shí), 就不會(huì)訪問原型中的name屬性了.
//?創(chuàng)建兩個(gè)對(duì)象,?并且調(diào)用方法
varperson1?=newPerson()
varperson2?=newPerson()
person1.sayHello()//?Coderwhy
person2.sayHello()//?Coderwhy
//?給person1實(shí)例添加屬性
person1.name?="Kobe"
person1.sayHello()//?Kobe,?來自實(shí)例
person2.sayHello()//?Coderwhy,?來自原型
通過hasOwnProperty判斷屬性屬于實(shí)例還是原型.
//?判斷屬性屬于誰
alert(person1.hasOwnProperty("name"))//?true
alert(person2.hasOwnProperty("name"))//?false
1.3. 簡潔的原型語法
簡潔語法概述:
如果按照前面的做法, 每添加一個(gè)原型屬性和方法, 都要敲一遍Person.prototype.
為了減少不必要的輸入, 另外也為了更好的封裝性, 更常用的做法是用一個(gè)包含所有屬性和方法的對(duì)象字面量來重寫整個(gè)原型對(duì)象.
字面量重寫原型對(duì)象:
//?定義Person構(gòu)造函數(shù)
functionPerson(){}
//?重寫Person的原型屬性
Person.prototype?=?{
name:"Coderwhy",
age:18,
height:1.88,
sayHello:function(){
alert(this.name)
}
}
注意:
我們將Person.prototype賦值了一個(gè)新的對(duì)象字面量, 最終結(jié)果和原來是一樣的娄周;
但是: constructor屬性不再指向Person了涕侈;
前面我們說過, 每創(chuàng)建一個(gè)函數(shù), 就會(huì)同時(shí)創(chuàng)建它的prototype對(duì)象, 這個(gè)對(duì)象也會(huì)自動(dòng)獲取constructor屬性;
而我們這里相當(dāng)于給prototype重新賦值了一個(gè)對(duì)象, 那么這個(gè)新對(duì)象的constructor屬性, 會(huì)指向Object構(gòu)造函數(shù), 而不是Person構(gòu)造函數(shù)了昆咽;
//?創(chuàng)建Person對(duì)象
varperson?=newPerson()
alert(person.constructor?===Object)//?true
alert(person.constructor?===?Person)//?false
alert(personinstanceofPerson)//?true
如果在某些情況下, 我們確實(shí)需要用到constructor的值, 可以手動(dòng)的給constructor賦值即可
//?定義Person構(gòu)造函數(shù)
functionPerson(){}
//?重寫Person的原型屬性
Person.prototype?=?{
constructor:?Person,
name:"Coderwhy",
age:18,
height:1.88,
sayHello:function(){
alert(this.name)
}
}
//?創(chuàng)建Person對(duì)象
varperson?=newPerson()
alert(person.constructor?===Object)//?false
alert(person.constructor?===?Person)//?true
alert(personinstanceofPerson)//?true
上面的方式雖然可以, 但是也會(huì)造成constructor的[[Enumerable]]特性被設(shè)置了true.
默認(rèn)情況下, 原生的constructor屬性是不可枚舉的.
如果希望解決這個(gè)問題, 就可以使用我們前面介紹的Object.defineProperty()函數(shù)了.
//?定義Person構(gòu)造函數(shù)
functionPerson(){}
//?重寫Person的原型屬性
Person.prototype?=?{
name:"Coderwhy",
age:18,
height:1.88,
sayHello:function(){
alert(this.name)
}
}
Object.defineProperty(Person.prototype,"constructor",?{
enumerable:false,
value:?Person
})
1.4. 修改原型屬性
考慮下面的代碼執(zhí)行是否會(huì)有問題:
//?定義Person構(gòu)造函數(shù)
functionPerson(){}
//?創(chuàng)建Person的對(duì)象
varperson?=newPerson()
//?給Person的原型添加方法
Person.prototype.sayHello?=function(){
alert("Hello?JavaScript")
}
//?調(diào)用方法
person.sayHello()
代碼解析:
我們發(fā)現(xiàn)代碼的執(zhí)行沒有任何問題.
因?yàn)樵趧?chuàng)建person的時(shí)候, person的__proto__也是指向的Person.prototype.
所以, 當(dāng)動(dòng)態(tài)的修改了Person.prototype中的sayHello屬性時(shí), person中也可以獲取到該屬性
圖解上面的過程:
img
我們?cè)賮砜聪旅娴拇a會(huì)不會(huì)有問題:
//?定義Person構(gòu)造函數(shù)
functionPerson(){}
//?創(chuàng)建Person的對(duì)象
varperson?=newPerson()
//?給Person的原型添加方法
Person.prototype?=?{
constructor:?Person,
sayHello:function(){
alert("Hello?JavaScript")
}
}
//?調(diào)用方法
person.sayHello()
代碼解析:
代碼是不能正常運(yùn)行的. 因?yàn)镻erson的prototype指向了一個(gè)新的對(duì)象.
而最初我們創(chuàng)建的person依然指向原來的原型對(duì)象, 原來的原型對(duì)象沒有sayHello()函數(shù).
當(dāng)然, 如果再次之后, 再創(chuàng)建的Person對(duì)象, 是可以調(diào)用sayHello()的, 但是再次之前創(chuàng)建的, 沒有該方法.
圖解上面的過程:
img
1.5. 原型對(duì)象問題
原型對(duì)象也有一些缺點(diǎn):
首先, 它不再有為構(gòu)造函數(shù)傳遞參數(shù)的環(huán)節(jié), 所有實(shí)例在默認(rèn)情況下都將有相同的屬性值.
另外, 原型中所有的屬性是被很多實(shí)例共享的, 這種共享對(duì)于函數(shù)來說非常適合, 對(duì)于基本屬性通常情況下也不會(huì)有問題. (因?yàn)橥ㄟ^person.name直接修改時(shí), 會(huì)在實(shí)例上重新創(chuàng)建該屬性名, 不會(huì)在原型上修改. 除非使用person.__proto__.name修改).
但是, 對(duì)于引用類型的實(shí)例, 就必然會(huì)存在問題.
考慮下面代碼的問題:
//?定義Person構(gòu)造函數(shù)
functionPerson(){}
//?設(shè)置Person原型
Person.prototype?=?{
constructor:?Person,
name:"Coderwhy",
age:18,
height:1.88,
hobby:?["Basketball","Football"],
sayHello:function(){
alert("Hello?JavaScript")
}
}
//?創(chuàng)建兩個(gè)person對(duì)象
varperson1?=newPerson()
varperson2?=newPerson()
alert(person1.hobby)//?Basketball,Football
alert(person2.hobby)//?Basketball,Football
person1.hobby.push("tennis")
alert(person1.hobby)//?Basketball,Football,tennis
alert(person2.hobby)//?Basketball,Football,tennis
OK, 我們會(huì)發(fā)現(xiàn), 我們明明給person1添加了一個(gè)愛好, 但是person2也被添加到一個(gè)愛好.
因?yàn)樗鼈兪枪蚕淼耐粋€(gè)數(shù)組.
但是, 我們希望每個(gè)人有屬于自己的愛好, 而不是所有的Person愛好都相同.
二. 組合構(gòu)造函數(shù)和原型模式
創(chuàng)建自定義類型的最常見方式驾凶,就是組合使用構(gòu)造函數(shù)模式與原型模式。
構(gòu)造函數(shù)模式用于定義實(shí)例屬性掷酗,而原型模式用于定義方法和共享的屬性调违。
結(jié)果,每個(gè)實(shí)例都會(huì)有自己的一份實(shí)例屬性的副本泻轰,但同時(shí)又共享著對(duì)方法的引用技肩,最大限度地節(jié)省了內(nèi)存。
另外浮声,這種混成模式還支持向構(gòu)造函數(shù)傳遞參數(shù)虚婿;可謂是集兩種模式之長。
組合構(gòu)造函數(shù)和原型模式的代碼
//?創(chuàng)建Person構(gòu)造函數(shù)
functionPerson(name,?age,?height){
this.name?=?name
this.age?=?age
this.height?=?height
this.hobby?=?["Basketball","Football"]
}
//?重新Peron的原型對(duì)象
Person.prototype?=?{
constructor:?Person,
sayHello:function(){
alert("Hello?JavaScript")
}
}
//?創(chuàng)建對(duì)象
varperson1?=newPerson("Coderwhy",18,1.88)
varperson2?=newPerson("Kobe",30,1.98)
//?測試是否共享了函數(shù)
alert(person1.sayHello?==?person2.sayHello)//?true
//?測試引用類型是否存在問題
person1.hobby.push("tennis")
alert(person1.hobby)
alert(person2.hobby)
如果理解了原型, 上面的代碼非常好理解.
person1和person2各有一份自己的屬性, 但是方法是共享的.
事實(shí)上, 還有一些其他的變種模式來實(shí)現(xiàn)基于對(duì)象的封裝. 但是這種方式是最常用的, 因此我們這里不再展開討論其他的模式. 后續(xù)需要我們?cè)偕钊胗懻?
JavaScript面向?qū)ο笤斀猓ㄈ?/p>
繼承是面向?qū)ο笾蟹浅V匾奶匦?
ES5中和類的實(shí)現(xiàn)一樣, 不能直接實(shí)現(xiàn)繼承. 實(shí)現(xiàn)繼承主要是依靠原型鏈來實(shí)現(xiàn)的泳挥。
一. 原型鏈
原型鏈?zhǔn)荅S5中實(shí)現(xiàn)繼承的主要手段, 因此相對(duì)比較重要, 我們需要深入理解原型鏈.
1.1. 深入理解原型鏈
先來回顧一下構(gòu)造函數(shù)然痊、原型和實(shí)例的關(guān)系:
每個(gè)構(gòu)造函數(shù)都有一個(gè)原型對(duì)象, 通過prototype指針指向該原型對(duì)象.
原型對(duì)象都包含一個(gè)指向構(gòu)造函數(shù)的指針, 通過constructor指針, 指向構(gòu)造函數(shù)
而實(shí)例都包含一個(gè)指向原型對(duì)象的內(nèi)部指針, 該內(nèi)部指針我們通常使用__proto__來描述.
思考如下情況:
我們知道, 可以通過Person.prototype = {}的方式來重寫原型對(duì)象.
假如, 我們后面賦值的不是一個(gè){}, 而是另外一個(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í)例與原型的鏈條。這就是所謂原型鏈的基本概念躬它。
有些抽象, 我們通過代碼來理解:
//?創(chuàng)建Person構(gòu)造函數(shù)
functionPerson(){
}
//?設(shè)置Animal的原型
Person.prototype?=?{
}
我們將代碼修改成原型鏈的形式:
//?1.創(chuàng)建Animal的構(gòu)造函數(shù)
functionAnimal(){
this.animalProperty?="Animal"
}
//?2.給Animal的原型中添加一個(gè)方法
Animal.prototype.animalFunction?=function(){
alert(this.animalProperty)
}
//?3.創(chuàng)建Person的構(gòu)造函數(shù)
functionPerson(){
this.personProperty?="Person"
}
//?4.給Person的原型對(duì)象重新賦值
Person.prototype?=newAnimal()
//?5.給Person添加屬于自己的方法
Person.prototype.personFunction?=function(){
alert(this.personProperty)
}
//?6.創(chuàng)建Person的實(shí)例
varperson?=newPerson()
person.animalFunction()
person.personFunction()
代碼解析:
代碼有一些復(fù)雜, 但是如果你希望學(xué)習(xí)好原型鏈, 必須耐心去看一看上面的代碼, 你會(huì)發(fā)現(xiàn)其實(shí)都是我們學(xué)習(xí)過的.
重點(diǎn)我們來看第4步代碼: 給Person.prototype賦值了一個(gè)Animal的實(shí)例. 也就是Person的原型變成了Animal的實(shí)例.
Animal實(shí)例本身有一個(gè)__proto__可以指向Animal的原型.
那么, 我們來思考一個(gè)問題: 如果現(xiàn)在搜索一個(gè)屬性或者方法, 這個(gè)時(shí)候會(huì)按照什么順序搜索呢?
第一步, 在person實(shí)例中搜索, 搜索到直接返回或者調(diào)用函數(shù). 如果沒有執(zhí)行第二步.
第二步, 在Person的原型中搜索, Person的原型是誰? Animal的實(shí)例. 所以會(huì)在Animal的實(shí)例中搜索, 無論是屬性還是方法, 如果搜索到則直接返回或者執(zhí)行. 如果沒有, 執(zhí)行第三步.
第三步, 在Animal的原型中搜索, 搜索到返回或者執(zhí)行, 如果沒有, 搜索結(jié)束. (當(dāng)然其實(shí)還有Object, 但是先不考慮)
畫圖解析可能更加清晰:
當(dāng)代碼執(zhí)行到第3步(上面代碼的序號(hào))的時(shí)候, 如圖所示:
img
當(dāng)代碼執(zhí)行第4步(上面代碼的序號(hào))時(shí), 發(fā)生了如圖所示的變化
注意圖片中的紅色線, 原來指向的是誰, 現(xiàn)在指向的是誰.
img
代碼繼續(xù)執(zhí)行
Person.prototype.personFunction = function (){}
當(dāng)執(zhí)行第5步, 也就是給Person的原型賦值了一個(gè)函數(shù)時(shí), 事實(shí)上在給new Animal(Animal的實(shí)例)賦值了一個(gè)新的方法.
img
代碼繼續(xù)執(zhí)行, 我們創(chuàng)建了一個(gè)Person對(duì)象
創(chuàng)建Person對(duì)象, person對(duì)象會(huì)有自己的屬性, personProperty.
另外, person對(duì)象有一個(gè)__prototype__指向Person的原型.
Person的原型是誰呢? 就是我們之前的new Animal(Animal的一個(gè)實(shí)例), 所以會(huì)指向它.
原型鏈簡單總結(jié):
通過實(shí)現(xiàn)原型鏈腾啥,本質(zhì)上擴(kuò)展了本章前面介紹的原型搜索機(jī)制。
當(dāng)以讀取模式訪問一個(gè)實(shí)例屬性時(shí)冯吓,首先會(huì)在實(shí)例中搜索該屬性倘待。如果沒有找到該屬性,則會(huì)繼續(xù)搜索實(shí)例的原型组贺。在通過原型鏈實(shí)現(xiàn)繼承的情況下延柠,搜索過程就得以沿著原型鏈繼續(xù)向上。
在找不到屬性或方法的情況下锣披,搜索過程總是要一環(huán)一環(huán)地前行到原型鏈末端才會(huì)停下來。
1.2. 原型和實(shí)例的關(guān)系
如果我們希望確定原型和實(shí)例之間的關(guān)系, 有兩種方式:
第一種方式是使用instanceof操作符贿条,只要用這個(gè)操作符來測試實(shí)例與原型鏈中出現(xiàn)過的構(gòu)造函數(shù)雹仿,結(jié)果就會(huì)返回true。
第二種方式是使用isPrototypeOf()方法整以。同樣胧辽,只要是原型鏈中出現(xiàn)過的原型,都可以說是該原型鏈所派生的實(shí)例的原型公黑,因此isPrototypeOf()方法也會(huì)返回true
instanceof操作符
//?instanceof
alert(personinstanceofObject)//?true
alert(personinstanceofAnimal)//?true
alert(personinstanceofPerson)//?true
isPrototypeOf()函數(shù)
//?isPrototypeOf函數(shù)
alert("isPrototypeOf函數(shù)函數(shù)")
alert(Object.prototype.isPrototypeOf(person))//?true
alert(Animal.prototype.isPrototypeOf(person))//?true
alert(Person.prototype.isPrototypeOf(person))//?true
1.3. 添加新的方法
添加新的方法
在第5步操作中, 我們?yōu)樽宇愋吞砑恿艘粋€(gè)新的方法. 但是這里有一個(gè)注意點(diǎn).
無論是子類中添加新的方法, 還是對(duì)父類中方法進(jìn)行重寫. 都一定要將添加方法的代碼, 放在替換原型語句之后.
否則, 我們添加的方法將會(huì)無效.
錯(cuò)誤代碼引起的代碼:
//?1.定義Animal的構(gòu)造函數(shù)
functionAnimal(){
this.animalProperty?="Animal"
}
//?2.給Animal添加方法
Animal.prototype.animalFunction?=function(){
alert(this.animalProperty)
}
//?3.定義Person的構(gòu)造函數(shù)
functionPerson(){
this.personProperty?="Person"
}
//?4.給Person添加方法
Person.prototype.personFunction?=function(){
alert(this.personProperty)
}
//?5.給Person賦值新的原型對(duì)象
Person.prototype?=newAnimal()
//?6.創(chuàng)建Person對(duì)象,?并且調(diào)用方法
varperson?=newPerson()
person.personFunction()//?不會(huì)有任何彈窗,?因?yàn)檎也坏皆摲椒?/p>
代碼解析:
執(zhí)行上面的代碼不會(huì)出現(xiàn)任何的彈窗, 因?yàn)槲覀兲砑拥姆椒ㄊ菬o效的, 被賦值的新的原型覆蓋了.
正確的辦法是將第4步和第5步操作換一下位置即可.
總結(jié)
其實(shí)這個(gè)問題沒什么好說的, 只要你理解了原型鏈(好好看看我上面畫的圖, 或者自己畫一下圖)
但是, 切記在看圖的過程中一樣掃過, 因?yàn)檫@會(huì)讓你錯(cuò)過很多細(xì)節(jié), 對(duì)原型鏈的理解就會(huì)出現(xiàn)問題.
1.4. 原型鏈的問題
原型鏈對(duì)于繼承來說:
原型鏈似乎對(duì)初學(xué)JavaScript原型的人來說, 已經(jīng)算是比較高明的設(shè)計(jì)技巧了, 有些人理解起來都稍微有些麻煩.
但是, 這種設(shè)計(jì)還存在一些缺陷, 不是最理性的解決方案. (但是后續(xù)的解決方案也是依賴原型鏈, 無論如何都需要先理解它)
原型鏈存在的問題:
原型鏈存在最大的問題是關(guān)于引用類型的屬性.
通過上面的原型實(shí)現(xiàn)了繼承后, 子類的person對(duì)象繼承了(可以訪問)Animal實(shí)例中的屬性(animalProperty).
但是如果這個(gè)屬性是一個(gè)引用類型(比如數(shù)組或者其他引用類型), 就會(huì)出現(xiàn)問題.
引用類型的問題代碼:
//?1.定義Animal的構(gòu)造函數(shù)
functionAnimal(){
this.colors?=?["red","green"]
}
//?2.給Animal添加方法
Animal.prototype.animalFunction?=function(){
alert(this.colors)
}
//?3.定義Person的構(gòu)造函數(shù)
functionPerson(){
this.personProperty?="Person"
}
//?4.給Person賦值新的原型對(duì)象
Person.prototype?=newAnimal()
//?5.給Person添加方法
Person.prototype.personFunction?=function(){
alert(this.personProperty)
}
//?6.創(chuàng)建Person對(duì)象,?并且調(diào)用方法
varperson1?=newPerson()
varperson2?=newPerson()
alert(person1.colors)//?red,green
alert(person2.colors)//?red,green
person1.colors.push("blue")
alert(person1.colors)//?red,green,blue
alert(person2.colors)//?red,green,blue
代碼解析:
我們查看第6步的操作
創(chuàng)建了兩個(gè)對(duì)象, 并且查看了它們的colors屬性
修改了person1中的colors屬性, 添加了一個(gè)新的顏色blue
再次查看兩個(gè)對(duì)象的colors屬性, 會(huì)發(fā)現(xiàn)person2的colors屬性也發(fā)生了變化
兩個(gè)實(shí)例應(yīng)該是相互獨(dú)立的, 這樣的變化如果我們不制止將會(huì)在代碼中引發(fā)一些列問題.
原型鏈的其他問題:
在創(chuàng)建子類型的實(shí)例時(shí)邑商,不能向父類型的構(gòu)造函數(shù)中傳遞參數(shù)。
實(shí)際上凡蚜,應(yīng)該說是沒有辦法在不影響所有對(duì)象實(shí)例的情況下人断,給父類型的構(gòu)造函數(shù)傳遞參數(shù)。
從而可以修改父類型中屬性的值, 在創(chuàng)建構(gòu)造函數(shù)的時(shí)候就確定一個(gè)值.
二. 經(jīng)典繼承
為了解決原型鏈繼承中存在的問題, 開發(fā)人員提供了一種新的技術(shù): constructor stealing(有很多名稱: 借用構(gòu)造函數(shù)或經(jīng)典繼承或偽造對(duì)象), steal是偷竊的意思, 但是這里可以翻譯成借用.
2.1. 經(jīng)典繼承的思想
經(jīng)典繼承的做法非常簡單: 在子類型構(gòu)造函數(shù)的內(nèi)部調(diào)用父類型構(gòu)造函數(shù).
因?yàn)楹瘮?shù)可以在任意的時(shí)刻被調(diào)用
因此通過apply()和call()方法也可以在新創(chuàng)建的對(duì)象上執(zhí)行構(gòu)造函數(shù).
經(jīng)典繼承代碼如下:
//?創(chuàng)建Animal的構(gòu)造函數(shù)
functionAnimal(){
this.colors?=?["red","green"]
}
//?創(chuàng)建Person的構(gòu)造函數(shù)
functionPerson(){
//?繼承Animal的屬性
Animal.call(this)
//?給自己的屬性賦值
this.name?="Coderwhy"
}
//?創(chuàng)建Person對(duì)象
varperson1?=newPerson()
varperson2?=newPerson()
alert(person1.colors)//?red,greem
alert(person2.colors)//?red,greem
person1.colors.push("blue")
alert(person1.colors)//?red,green,blue
alert(person2.colors)//?red,green
代碼解析:
我們通過在Person構(gòu)造函數(shù)中, 使用call函數(shù), 將this傳遞進(jìn)去.
這個(gè)時(shí)候, 當(dāng)Animal中有相關(guān)屬性初始化時(shí), 就會(huì)在this對(duì)象上進(jìn)行初始化操作.
這樣就實(shí)現(xiàn)了類似于繼承Animal屬性的效果.
這個(gè)時(shí)候, 我們也可以傳遞參數(shù), 修改上面的代碼:
//?創(chuàng)建Animal構(gòu)造函數(shù)
functionAnimal(age){
this.age?=?age
}
//?創(chuàng)建Person構(gòu)造函數(shù)
functionPerson(name,?age){
Animal.call(this,?age)
this.name?=?name
}
//?創(chuàng)建Person對(duì)象
varperson?=newPerson("Coderwhy",18)
alert(person.name)
alert(person.age)
2.2. 經(jīng)典繼承的問題
經(jīng)典繼承的問題:
對(duì)于經(jīng)典繼承理解比較深入, 你已經(jīng)能發(fā)現(xiàn): 經(jīng)典繼承只有屬性的繼承, 無法實(shí)現(xiàn)方法的繼承.
因?yàn)檎{(diào)用call函數(shù), 將this傳遞進(jìn)去, 只能將父構(gòu)造函數(shù)中的屬性初始化到this中.
但是如果函數(shù)存在于父構(gòu)造函數(shù)的原型對(duì)象中, this中是不會(huì)有對(duì)應(yīng)的方法的.
回顧原型鏈和經(jīng)典繼承:
原型鏈存在的問題是引用類型問題和無法傳遞參數(shù), 但是方法可以被繼承
經(jīng)典繼承是引用類型沒有問題, 也可以傳遞參數(shù), 但是方法無法被繼承.
怎么辦呢? 將兩者結(jié)合起來怎么樣?
三. 組合繼承
如果你認(rèn)識(shí)清楚了上面兩種實(shí)現(xiàn)繼承的方式存在的問題, 就可以很好的理解組合繼承了.
組合繼承(combination inheritance, 有時(shí)候也稱為偽經(jīng)典繼承), 就是將原型鏈和經(jīng)典繼承組合在一起, 從而發(fā)揮各自的優(yōu)點(diǎn).
3.1. 組合繼承的思想
組合繼承:
組合繼承就是發(fā)揮原型鏈和經(jīng)典繼承各自的優(yōu)點(diǎn)來完成繼承的實(shí)現(xiàn).
使用原型鏈實(shí)現(xiàn)對(duì)原型屬性和方法的繼承.
通過經(jīng)典繼承實(shí)現(xiàn)對(duì)實(shí)例屬性的繼承, 以及可以在構(gòu)造函數(shù)中傳遞參數(shù).
組合繼承的代碼:
//?1.創(chuàng)建構(gòu)造函數(shù)的階段
//?1.1.創(chuàng)建Animal的構(gòu)造函數(shù)
functionAnimal(age){
this.age?=?age
this.colors?=?["red","green"]
}
//?1.2.給Animal添加方法
Animal.prototype.animalFunction?=function(){
alert("Hello?Animal")
}
//?1.3.創(chuàng)建Person的構(gòu)造函數(shù)
functionPerson(name,?age){
Animal.call(this,?age)
this.name?=?name
}
//?1.4.給Person的原型對(duì)象重新賦值
Person.prototype?=newAnimal(0)
//?1.5.給Person添加方法
Person.prototype.personFunction?=function(){
alert("Hello?Person")
}
//?2.驗(yàn)證和使用的代碼
//?2.1.創(chuàng)建Person對(duì)象
varperson1?=newPerson("Coderwhy",18)
varperson2?=newPerson("Kobe",30)
//?2.2.驗(yàn)證屬性
alert(person1.name?+"-"+?person1.age)//?Coderwhy,18
alert(person2.name?+"-"+?person2.age)//?Kobe,30
//?2.3.驗(yàn)證方法的調(diào)用
person1.animalFunction()//?Hello?Animal
person1.personFunction()//?Hello?Person
//?2.4.驗(yàn)證引用屬性的問題
person1.colors.push("blue")
alert(person1.colors)//?red,green,blue
alert(person2.colors)//?red,green
代碼解析:
根據(jù)前面學(xué)習(xí)的知識(shí), 結(jié)合當(dāng)前的代碼, 大家應(yīng)該可以理解上述代碼的含義.
但是我還是建議大家一定要多手動(dòng)自己來敲代碼, 來理解其中每一個(gè)步驟.
記住: 看懂, 聽懂不一定真的懂, 自己可以寫出來, 才是真的懂了.
3.2. 組合繼承的分析
組合繼承是JavaScript最常用的繼承模式之一.
如果你理解到這里, 點(diǎn)到為止, 那么組合來實(shí)現(xiàn)繼承只能說問題不大.
但是它依然不是很完美, 存在一些問題不大的問題.(不成問題的問題, 基本一詞基本可用, 但基本不用)
組合繼承存在什么問題呢?
組合繼承最大的問題就是無論在什么情況下, 都會(huì)調(diào)用兩次父類構(gòu)造函數(shù).
一次在創(chuàng)建子類原型的時(shí)候
另一次在子類構(gòu)造函數(shù)內(nèi)部(也就是每次創(chuàng)建子類實(shí)例的時(shí)候).
另外, 如果你仔細(xì)按照我的流程走了上面的每一個(gè)步驟, 你會(huì)發(fā)現(xiàn): 所有的子類實(shí)例事實(shí)上會(huì)擁有兩份父類的屬性
一份在當(dāng)前的實(shí)例自己里面(也就是person本身的), 另一份在子類對(duì)應(yīng)的原型對(duì)象中(也就是person.__proto__里面)
當(dāng)然, 這兩份屬性我們無需擔(dān)心訪問出現(xiàn)問題, 因?yàn)槟J(rèn)一定是訪問實(shí)例本身這一部分的.
怎么解決呢?
看起來組合繼承也不是非常完美的解決方案, 雖然也可以應(yīng)用.
JavaScript面向?qū)ο笤斀猓ㄋ模?/p>
在上一篇中, 我們討論了ES5中, 實(shí)現(xiàn)繼承的一些方式.
在最后, 我們說了組合繼承是相對(duì)完美的解決方案, 但是它也存在一些問題.
這篇文章, 我們就通過某種新的模式, 給出一種目前使用最多, 也是我們最終的解決方案.
一. 原型式繼承
1.1. 原型式繼承的思想
原型式繼承的淵源
這種模式要從道格拉斯·克羅克福德(Douglas Crockford, 著名的前端大師, JSON的創(chuàng)立者)在2006年寫的一篇文章說起: Prototypal Inheritance in JavaScript(在JS中使用原型式繼承)
在這篇文章中, 它介紹了一種繼承方法, 而且這種繼承方法不是通過構(gòu)造函數(shù)來實(shí)現(xiàn)的.
為了理解這種方式, 我們先再次回顧一下JavaScript想實(shí)現(xiàn)繼承的目的: 重復(fù)利用另外一個(gè)對(duì)象的屬性和方法.
原型式繼承的核心函數(shù):
//?封裝object()函數(shù)
functionobject(o){
functionF(){}
F.prototype?=?o
returnnewF()
}
代碼解析:
在object()函數(shù)內(nèi)部, 先創(chuàng)建一個(gè)臨時(shí)的構(gòu)造函數(shù).
然后將傳遞的對(duì)象作為這個(gè)構(gòu)造函數(shù)的原型
最后返回了這個(gè)臨時(shí)類型的一個(gè)新的實(shí)例.
事實(shí)上, object()對(duì)傳入的對(duì)象執(zhí)行了一次淺復(fù)制.
1.2. 原型式繼承的使用
使用原型式繼承:
//?使用原生式繼承
varperson?=?{
name:"Coderwhy",
colors:?["red","green"]
}
//?通過person去創(chuàng)建另外一個(gè)對(duì)象
varperson1?=?object(person)
person1.name?="Kobe"
person1.colors.push("blue")
alert(person1.name)//?Kobe
alert(person1.colors)//?red,green,blue
alert(person.name)//?Coderwhy
alert(person.colors)//?red,green,blue
代碼解析:
這種方式和我們傳統(tǒng)意義上理解的繼承有些不同. 它做的事情是通過一個(gè)對(duì)象去創(chuàng)建另外一個(gè)對(duì)象.(利用person去創(chuàng)建person1)
當(dāng)然, person1中繼承過來的屬性是放在了自己的原型對(duì)象中的.
也可以給person1自己再次添加name屬性, 這個(gè)時(shí)候name才是在實(shí)例本身中.
但是如果是修改或者添加引用類型的內(nèi)容, 還是會(huì)引起連鎖反應(yīng).
可能暫時(shí)你看不到這些代碼的意義, 但是這些代碼是我們后續(xù)終極方案的前提思想, 所以先看看和練習(xí)一下這些代碼.
針對(duì)這種思想, ES5中新增了Object.create()方法來規(guī)范化了原型式繼承.
也就是上面的代碼可以修改成這樣.(只是將object函數(shù)修改成了Object.create)
//?使用原生式繼承
varperson?=?{
name:"Coderwhy",
colors:?["red","green"]
}
//?通過person去創(chuàng)建另外一個(gè)對(duì)象
varperson1?=Object.create(person)
person1.name?="Kobe"
person1.colors.push("blue")
alert(person1.name)//?Kobe
alert(person1.colors)//?red,green,blue
alert(person.name)//?Coderwhy
alert(person.colors)//?red,green,blue
Object.create()還可以傳入第二個(gè)參數(shù):
第二個(gè)參數(shù)用于每個(gè)屬性的自定義描述.
比如person1的name我們希望修改成"Kobe", 就可以這樣來做
//?使用原型式繼承
varperson?=?{
name:"Coderwhy",
colors:?["red","green"]
}
//?通過person去創(chuàng)建另外一個(gè)對(duì)象
varperson1?=Object.create(person,?{
name:?{
value:"Kobe"
}
})
person1.colors.push("blue")
alert(person1.name)//?Kobe
alert(person1.colors)//?red,green,blue
alert(person.name)//?Coderwhy
alert(person.colors)//?red,green,blue
1.3. 原型式繼承的問題
原型式繼承的的優(yōu)點(diǎn)和缺點(diǎn):
如果我們只是希望一個(gè)對(duì)象和另一個(gè)對(duì)象保持類似的情況下, 原型式繼承完全可以勝任, 這是它的優(yōu)勢.
但是, 原型式繼承依然存在屬性共享的問題, 就像使用原型鏈一樣.
二. 寄生式繼承
2.1. 寄生式繼承的思想
寄生式(Parasitic)繼承
寄生式(Parasitic)繼承是與原型式繼承緊密相關(guān)的一種思想, 并且同樣由道格拉斯·克羅克福德(Douglas Crockford)提出和推廣的
寄生式繼承的思路是結(jié)合原型類繼承和工廠模式的一種方式.
即創(chuàng)建一個(gè)封裝繼承過程的函數(shù), 該函數(shù)在內(nèi)部以某種方式來增強(qiáng)對(duì)象, 最后再將這個(gè)對(duì)象返回.
寄生式函數(shù)多增加了一個(gè)核心函數(shù):
//?封裝object函數(shù)
functionobject(o){
functionF(){}
F.prototype?=?o
returnnewF()
}
//?封裝創(chuàng)建新對(duì)象的函數(shù)
functioncreateAnother(original){
varclone?=?object(original)
clone.sayHello?=function(){
alert("Hello?JavaScript")
}
returnclone
}
2.2. 寄生式繼承的應(yīng)用
我們來使用一下寄生式繼承
//?person對(duì)象
varperson?=?{
name:"Coderwhy",
colors:?["red","green"]
}
//?新的對(duì)象
varperson1?=?createAnother(person)
person1.sayHello()
代碼解讀:
我們基于person對(duì)象, 創(chuàng)建了另外一個(gè)對(duì)象person1.
在最新的person1對(duì)象中, 不僅會(huì)擁有person的屬性和方法, 而且還有自己定義的方法.
2.3. 寄生式繼承的問題
寄生式繼承存在的問題:
寄生式繼承和原型式繼承存在一樣的問題, 引用類型會(huì)共享. (因?yàn)槭窃谠褪嚼^承基礎(chǔ)上的一種封裝)
另外寄生式繼承還存在函數(shù)無法復(fù)用的問題, 因?yàn)槊看蝐reateAnother一個(gè)新的對(duì)象, 都需要重新定義新的函數(shù).
三. 寄生組合式繼承
3.1. 寄生組合式繼承的思想
寄生組合式繼承
現(xiàn)在我們來回顧一下之前提出的比較理想的組合繼承
組合繼承是比較理想的繼承方式, 但是存在兩個(gè)問題:
問題一: 構(gòu)造函數(shù)會(huì)被調(diào)用兩次: 一次在創(chuàng)建子類型原型對(duì)象的時(shí)候, 一次在創(chuàng)建子類型實(shí)例的時(shí)候.
問題二: 父類型中的屬性會(huì)有兩份: 一份在原型對(duì)象中, 一份在子類型實(shí)例中.
事實(shí)上, 我們現(xiàn)在可以利用寄生式繼承將這兩個(gè)問題給解決掉.
你需要先明確一點(diǎn): 當(dāng)我們?cè)谧宇愋偷臉?gòu)造函數(shù)中調(diào)用父類型.call(this, 參數(shù))這個(gè)函數(shù)的時(shí)候, 就會(huì)將父類型中的屬性和方法復(fù)制一份到了子類型中. 所以父類型本身里面的內(nèi)容, 我們不再需要.
這個(gè)時(shí)候, 我們還需要獲取到一份父類型的原型對(duì)象中的屬性和方法.
能不能直接讓子類型的原型對(duì)象 = 父類型的原型對(duì)象呢?
不要這么做, 因?yàn)檫@么做意味著以后修改了子類型原型對(duì)象的某個(gè)引用類型的時(shí)候, 父類型原生對(duì)象的引用類型也會(huì)被修改.
我們使用前面的寄生式思想就可以了.
寄生組合式的核心代碼:
//?定義object函數(shù)
functionobject(o){
functionF(){}
F.prototype?=?o
returnnewF()
}
//?定義寄生式核心函數(shù)
functioninhreitPrototype(subType,?superType){
varprototype?=?object(superType.prototype)
prototype.constructor?=?subType
subType.prototype?=?prototype
}
3.2. 寄生組合式繼承的應(yīng)用
直接給出使用的代碼, 也是我們以后使用繼承的終極方式
//?定義Animal構(gòu)造函數(shù)
functionAnimal(age){
this.age?=?age
this.colors?=?["red","green"]
}
//?給Animal添加方法
Animal.prototype.animalFunction?=function(){
alert("Hello?Animal")
}
//?定義Person構(gòu)造函數(shù)
functionPerson(name,?age){
Animal.call(this,?age)
this.name?=?name
}
//?使用寄生組合式核心函數(shù)
inhreitPrototype(Person,?Animal)
//?給Person添加方法
Person.prototype.personFunction?=function(){
alert("Hello?Person")
}
代碼的優(yōu)點(diǎn):
這種方式的高效體現(xiàn)在現(xiàn)在它只調(diào)用了一次Animal的構(gòu)造函數(shù).
并且也避免了在原型上面多出的多余屬性, 而且原型之間不會(huì)產(chǎn)生任何的干擾(子類型原型和父類型原型之間).
在ES5中, 普遍認(rèn)為寄生組合式繼承是最理想的繼承范式.