例子
我們生成兩個構(gòu)造函數(shù),后面的例子都是讓‘’貓‘’繼承‘’動物‘’的所有屬性和方法关划。
- 動物(為了更好的理解各種繼承,這里給動物附上了基本類型和引用類型)
function Animal() {
this.species = "動物"
this.do = ['運(yùn)動', '繁殖']
}
- 貓
function Cat(name, color) {
this.name = name
this.color = color
}
1.簡單的原型鏈
這可能是最簡單直觀的一種實(shí)現(xiàn)繼承方式了
1.1 實(shí)現(xiàn)方法
function Animal() {
this.species = "動物"
this.do = ['運(yùn)動', '繁殖']
}
function Cat(name, color) {
this.name = name
this.color = color
}
Cat.prototype = new Animal //重點(diǎn)G涛汀V邸!W手选调榄!
Cat.prototype.constructor = Cat
var cat1 = new Cat('小黃', '黃色')
console.log(cat1.species) // 動物
console.log(cat1.do) // [ '運(yùn)動', '繁殖' ]
1.2 核心
這種方法的核心就這一句話:Cat.prototype = new Animal 也就是拿父類實(shí)例來充當(dāng)子類原型對象
1.3 優(yōu)缺點(diǎn)
然而這個方法雖然簡單但是有一個很嚴(yán)重的問題:在我們修改一個實(shí)例的屬性時,其他的也隨之改變呵扛。
var cat1 = new Cat('小黃', '黃色')
var cat2 = new Cat('小白', '白色')
cat1.species = '哺乳動物'
cat1.do.push('呼吸')
console.log(cat1.species) // 哺乳動物
console.log(cat2.species) // 動物
console.log(cat1.do) // [ '運(yùn)動', '繁殖', '呼吸' ]
console.log(cat2.do) // [ '運(yùn)動', '繁殖', '呼吸' ]
- 優(yōu)點(diǎn)
- 容易實(shí)現(xiàn)
- 缺點(diǎn)
修改cat1.do后cat2.do也變了每庆,因?yàn)閬碜栽蛯ο蟮囊脤傩允撬袑?shí)例共享的。
可以這樣理解:執(zhí)行cat1.do.push('呼吸');先對cat1進(jìn)行屬性查找今穿,找遍了實(shí)例屬性(在本例中沒有實(shí)例屬性)缤灵,沒找到,就開始順著原型鏈向上找,拿到了cat1的原型對象腮出,一搜身帖鸦,發(fā)現(xiàn)有do屬性。于是給do末尾插入了'呼吸'胚嘲,所以sub2.do也變了創(chuàng)建子類實(shí)例時富蓄,無法向父類構(gòu)造函數(shù)傳參
1.4 繼承鏈的紊亂問題
Cat.prototype = new Animal
任何一個prototype對象都有一個constructor屬性,指向它的構(gòu)造函數(shù)慢逾。如果沒有"Cat.prototype = new Animal();"這一行立倍,Cat.prototype.constructor是指向Cat的。加了這一行以后侣滩,Cat.prototype.constructor指向Animal口注。
alert(Cat.prototype.constructor == Animal) //true
更重要的是,每一個實(shí)例也有一個constructor屬性君珠,默認(rèn)調(diào)用prototype對象的constructor屬性寝志。因此,在運(yùn)行"Cat.prototype = new Animal();"這一行之后策添,cat1.constructor也指向Animal材部!
alert(cat1.constructor == Cat.prototype.constructor) // true
這顯然會導(dǎo)致繼承鏈的紊亂(cat1明明是用構(gòu)造函數(shù)Cat生成的),因此我們必須手動糾正唯竹,將Cat.prototype對象的constructor值改為Cat乐导。
Cat.prototype.constructor = Cat
這是很重要的一點(diǎn),編程時務(wù)必要遵守浸颓。下文都遵循這一點(diǎn)物臂,即如果替換了prototype對象,那么产上,下一步必然是為新的prototype對象加上constructor屬性棵磷,并將這個屬性指回原來的構(gòu)造函數(shù)。
2. 借用構(gòu)造函數(shù)
使用call或apply方法晋涣,將父對象的構(gòu)造函數(shù)綁定在子對象上仪媒,即在子對象構(gòu)造函數(shù)中加一行:Animal.apply(this, arguments)
2.1 實(shí)現(xiàn)方法
function Animal() {
this.species = "動物"
this.do = ['運(yùn)動', '繁殖']
}
function Cat(name, color) {
Animal.call(this, arguments) ///重點(diǎn)!P蝗怠K惴浴!
this.name = name
this.color = color
}
var cat1 = new Cat('小黃', '黃色')
console.log(cat1.species) // 動物
console.log(cat1.do) // [ '運(yùn)動', '繁殖' ]
2.2 核心
借父類的構(gòu)造函數(shù)來增強(qiáng)子類實(shí)例撇贺,等于是把父類的實(shí)例屬性復(fù)制了一份給子類實(shí)例裝上了(完全沒有用到原型)
2.3 優(yōu)缺點(diǎn)
var cat1 = new Cat('小黃', '黃色')
var cat2 = new Cat('小白', '白色')
cat1.species = '哺乳動物'
cat1.do.push('呼吸')
console.log(cat1.species) // 哺乳動物
console.log(cat2.species) // 動物
console.log(cat1.do) // [ '運(yùn)動', '繁殖', '呼吸' ]
console.log(cat2.do) // [ '運(yùn)動', '繁殖' ]
- 優(yōu)點(diǎn):
- 解決了子類實(shí)例共享父類引用屬性的問題
- 創(chuàng)建子類實(shí)例時赌莺,可以向父類構(gòu)造函數(shù)傳參
- 缺點(diǎn):
- 無法實(shí)現(xiàn)函數(shù)復(fù)用冰抢,過多的占用內(nèi)存松嘶。
- 創(chuàng)建子類實(shí)例時,無法向父類構(gòu)造函數(shù)傳參
3. 組合繼承(偽經(jīng)典繼承)
將原型鏈和借用構(gòu)造函數(shù)的技術(shù)組合起來挎扰,發(fā)揮二者之長:使用原型鏈實(shí)現(xiàn)對原型屬性和方法的繼承翠订,而通過借用構(gòu)造函數(shù)來實(shí)現(xiàn)對實(shí)例屬性的繼承巢音。這樣,既通過在原型上定義的方法實(shí)現(xiàn)了函數(shù)復(fù)用尽超,又能夠保證每個實(shí)例都有它自己的屬性官撼。是實(shí)現(xiàn)繼承最常用的方式。
3.1 實(shí)現(xiàn)方法
function Animal() {
this.species = "動物"
this.do = ['運(yùn)動', '繁殖']
}
function Cat(name, color) {
Animal.call(this, arguments)//重點(diǎn)K扑0列濉!巩踏!
this.name = name
this.color = color
}
Cat.prototype = new Animal//重點(diǎn)M核小!H怼菠净!
Cat.prototype.constructor = Cat
var cat1 = new Cat('小黃', '黃色')
console.log(cat1.species) // 動物
console.log(cat1.do) // [ '運(yùn)動', '繁殖' ]
3.2 核心
把實(shí)例函數(shù)都放在原型對象上,以實(shí)現(xiàn)函數(shù)復(fù)用彪杉。同時還要保留借用構(gòu)造函數(shù)方式的優(yōu)點(diǎn)毅往,通過Animal.call(this);繼承父類的基本屬性和引用屬性并保留能傳參的優(yōu)點(diǎn);通過Cat.prototype = new Animal繼承父類函數(shù)派近,實(shí)現(xiàn)函數(shù)復(fù)用攀唯。
3.3 優(yōu)缺點(diǎn)
- 優(yōu)點(diǎn):
- 不存在引用屬性共享問題
- 可傳參
- 函數(shù)可復(fù)用
- 缺點(diǎn):
- 子類原型上有一份多余的父類實(shí)例屬性,因?yàn)楦割悩?gòu)造函數(shù)被調(diào)用了兩次渴丸,生成了兩份革答。(私有屬性一份,原型里面一份)
4. 原型式
道格拉斯·克羅克福德在2006年寫了一篇文章曙强,Prototypal Inheritance in JavaScript(JavaScript中的原型式繼承)残拐。在這篇文章中,他介紹了一種實(shí)現(xiàn)繼承的方法碟嘴,這種方法并沒有使用嚴(yán)格意義上的構(gòu)造函數(shù)溪食。他的想法是借助原型可以基于已有的對象創(chuàng)建新對象,同時還不必因此創(chuàng)建自定義類型娜扇,為了達(dá)到這個目的错沃,他給出了如下函數(shù)。
function object(o) {
function F() {}
f.prototype = o
return new F()
}
在object()函數(shù)內(nèi)部雀瓢,先創(chuàng)建了一個臨時性的構(gòu)造函數(shù)枢析,然后將傳入的對象作為這個構(gòu)造函數(shù)的原型,最后返回了這個臨時類型的一個新實(shí)例刃麸。從本質(zhì)上講醒叁,object()對傳入其中的對象執(zhí)行了一次淺拷貝。
4.1 實(shí)現(xiàn)方法
function object(o) {
function F() {}
F.prototype = o
return new F()
}
function Animal() {
this.species = "動物"
this.do = ['運(yùn)動', '繁殖']
}
var Animal1 = new Animal
var cat1 = object(Animal1)//重點(diǎn)!0颜印0∫住!
cat1.name = '小黃'
cat1.color = '黃色'
console.log(cat1.species) //動物
console.log(cat1.do) //["運(yùn)動", "繁殖"]
4.2 核心
核心就是通過一個函數(shù)來得到一個空的新對象饮睬,再在空對象的基礎(chǔ)上添加需要的方法(實(shí)例屬性)
4.3 優(yōu)缺點(diǎn)
- 優(yōu)點(diǎn):
- 從已有對象衍生新對象租谈,不需要創(chuàng)建自定義類型。
- 缺點(diǎn):
- 原型引用屬性會被所有實(shí)例共享捆愁,因?yàn)槭怯谜麄€父類對象來充當(dāng)了子類
原型對象割去,所以這個缺陷無可避免 - 無法實(shí)現(xiàn)代碼復(fù)用
5. 寄生式
寄生式在我看來和原型式差別不大,只是把對空對象私有屬性的添加封裝成了一個函數(shù)昼丑。
5.1 實(shí)現(xiàn)方法
function object(o) {
function F() {}
F.prototype = o
return new F()
}
function Animal() {
this.species = "動物"
this.do = ['運(yùn)動', '繁殖']
}
function getCatObject(obj) {
var clone = object(obj)//重點(diǎn)=俎帧!7恕页慷!
clone.name = '小黃'
clone.color = '黃色'
return clone
}
var cat1 = getCatObject(new Animal)
console.log(cat1.species) //動物
console.log(cat1.do) //["運(yùn)動", "繁殖"]
5.2 核心
只是給原型式繼承套了一個殼子而已。
對于寄生式的理解:創(chuàng)建新對象 -> 增強(qiáng) -> 返回該對象胁附,這樣的過程叫寄生式繼承酒繁,新對象是如何創(chuàng)建的并不重要。
5.3 優(yōu)缺點(diǎn)
- 優(yōu)點(diǎn):
- 不需要創(chuàng)建自定義類型控妻。
- 缺點(diǎn):
- 無法實(shí)現(xiàn)代碼復(fù)用
6. 寄生組合繼承
前面說過州袒,組合繼承是JavaScript 最常用的繼承模式;不過弓候,它也有自己的不足郎哭。組合繼承最大的問題就是無論什么情況下,都會調(diào)用兩次超類型構(gòu)造函數(shù):一次是在創(chuàng)建子類型原型的時候菇存,另一次是在子類型構(gòu)造函數(shù)內(nèi)部夸研。也就是會出現(xiàn)這種情況:
我們發(fā)現(xiàn)在私有屬性和原型里面都有name和do的屬性,這是因?yàn)檎{(diào)用了兩次構(gòu)造函數(shù)造成的后果依鸥,這必然會過多占用內(nèi)存亥至。
寄生組合繼承完美的解決了這個問題。
6.1 實(shí)現(xiàn)方法
function object(o) {
function F() {}
F.prototype = o
return new F()
}
function Animal() {
this.species = "動物"
this.do = ['運(yùn)動', '繁殖']
}
function Cat(name, color) {
Animal.call(this, arguments)//重點(diǎn)<佟=惆纭!衣吠!
this.name = name
this.color = color
}
var proto = object(Animal.prototype)//重點(diǎn)2杳簟!8壳巍惊搏!
proto.constructor = Cat//重點(diǎn)V椤!U陀臁!
Cat.prototype = proto//重點(diǎn)0浮K拚浮!才写!
var cat1 = new Cat()
console.log(cat1.species) //動物
console.log(cat1.do) //["運(yùn)動", "繁殖"]
6.2 核心
用object(Animal.prototype)切掉了原型對象上多余的那份父類實(shí)例屬性
6.3 優(yōu)缺點(diǎn)
- 優(yōu)點(diǎn):
- 幾乎完美
- 缺點(diǎn):
- 用起來有些麻煩葡兑,理論上沒有缺點(diǎn)。
7. ES5使用 Object.create 創(chuàng)建對象
ECMAScript 5 中引入了一個新方法:Object.create()
赞草《锏蹋可以調(diào)用這個方法來創(chuàng)建一個新對象。新對象的原型就是調(diào)用 create方法時傳入的第一個參數(shù):
var a = {a: 1};
// a ---> Object.prototype ---> null
var b = Object.create(a);
// b ---> a ---> Object.prototype ---> null
console.log(b.a); // 1 (繼承而來)
var c = Object.create(b);
// c ---> b ---> a ---> Object.prototype ---> null
var d = Object.create(null);
// d ---> null
console.log(d.hasOwnProperty); // undefined, 因?yàn)閐沒有繼承Object.prototype
8. ES6使用 class 關(guān)鍵字
ECMAScript6 引入了一套新的關(guān)鍵字用來實(shí)現(xiàn) class厨疙。使用基于類語言的開發(fā)人員會對這些結(jié)構(gòu)感到熟悉洲守,但它們是不一樣的。 JavaScript 仍然是基于原型的沾凄。這些新的關(guān)鍵字包括 class
, constructor
, static
, extends
, 和 super
.
例子如下:
class Animal {
constructor(species, canDo) {
this.species = '動物'
this.canDo = ['運(yùn)動', '繁殖']
}
}
class Cat extends Animal {
constructor(name, color) {
super()
this.name = name
this.color = color
}
}
var cat1 = new Cat('小黃', '黃色')
console.dir(cat1)