原型/原型鏈
JS是一門基于原型實(shí)現(xiàn)繼承的語(yǔ)言。那么举庶,什么是原型?基于原型實(shí)現(xiàn)的繼承又是怎么一回事揩抡?
原型(prototype)户侥,根據(jù)字面意思镀琉,可以理解為一件事物的模板。比如iPhone的原型是以前只能打電話蕊唐、發(fā)短信的功能機(jī)屋摔,這表示,iPhone也擁有打電話替梨、發(fā)短信的功能(繼承)钓试,但是相比它的原型又擁有了更多功能(可以擴(kuò)展更多功能)。這種關(guān)系有點(diǎn)類似于Java中的子類與父類的關(guān)系(Java是基于類的繼承副瀑,而Javascript是基于原型)弓熏。
在JS中,每一個(gè)函數(shù)(Function)都有一個(gè)prototype屬性糠睡,這個(gè)prototype對(duì)象有一個(gè)屬性constructor指向這個(gè)構(gòu)造函數(shù):
function Person() {
?
}
Person.prototype.constructor === Person // true
另外挽鞠,每個(gè)JS對(duì)象還有一個(gè)隱藏屬性proto,指向它的構(gòu)造函數(shù)的原型對(duì)象(prototype)狈孔,即:
var person = new Person();
person._proto_ === Person.prototype; // true
對(duì)象實(shí)例信认、構(gòu)造函數(shù)和原型的關(guān)系可以表示成下圖:
更進(jìn)一步的,我們知道Person.prototype也是一個(gè)對(duì)象均抽,那么它也擁有proto屬性嫁赏,指向Person.prototype構(gòu)造函數(shù)的原型對(duì)象,這個(gè)原型對(duì)象又有proto屬性…...通過(guò)這樣一個(gè)實(shí)例對(duì)象的proto指向構(gòu)造函數(shù)prototype油挥、prototype對(duì)象又擁有proto屬性的指向循環(huán)潦蝇,我們就可以建立起一條原型鏈。
person._proto_ => Person.prototype
Person.prototype._proto_ => Object.prototype
Object.prototype._proto_ === null // true
注意喘漏,所有原型鏈的終點(diǎn)是Object.prototype.proto护蝶,這個(gè)對(duì)象沒(méi)有對(duì)應(yīng)構(gòu)造函數(shù)的原型了,所以為null翩迈。
基于原型對(duì)象(prototype)和原型鏈持灰,我們就可以實(shí)現(xiàn)繼承。
繼承
按照《Javascript高級(jí)程序設(shè)計(jì)》中所寫负饲,在JS中實(shí)現(xiàn)繼承大致有6種方式:
一堤魁、借用構(gòu)造函數(shù)
子類型要如何擁有父類型的屬性呢?如果父類的屬性都是通過(guò)構(gòu)造函數(shù)定義的返十,那么最簡(jiǎn)單粗暴的方法當(dāng)然是直接在子類的構(gòu)造函數(shù)中調(diào)用父類的構(gòu)造函數(shù)妥泉,此時(shí)子類就具有了父類的所有屬性。
function SuperType(name) {
this.name = name;
this.colors = ['pink', 'blue', 'green'];
}
function SubType(name) {
SuperType.call(this, name);
}
let instance1 = new SubType('Yvette');
instance1.colors.push('yellow');
console.log(instance1.colors);//['pink', 'blue', 'green', yellow]
let instance2 = new SubType('Jack');
console.log(instance2.colors); //['pink', 'blue', 'green']
此時(shí)我們已經(jīng)讓SubType
擁有了SuperType
的屬性洞坑。
問(wèn)題來(lái)了盲链,如果我們想讓SubType
能夠直接復(fù)用/繼承SuperType
的方法,這種繼承的方式就無(wú)法實(shí)現(xiàn)了。因此這種方式是有缺陷的刽沾,在實(shí)踐中也不可能單純用它實(shí)現(xiàn)繼承本慕。
此時(shí)就需要我們前面鋪墊了很久的原型對(duì)象出場(chǎng)了。
二侧漓、原型鏈繼承
在JS中讀取一個(gè)實(shí)例屬性時(shí)锅尘,首先會(huì)在該實(shí)例上讀取,如果讀取不到需要的屬性布蔗,則會(huì)在實(shí)例的原型上搜索藤违。那么如果我們讓A類型的原型指向另一個(gè)B類型,在A類型上讀取不到的屬性就可以接著去A類型的原型(也就是B類型)去讀取纵揍,更進(jìn)一步的會(huì)去搜過(guò)B類型的原型顿乒,一直到原型鏈的末端。這就是原型鏈繼承骡男。
因此我們想要SubType
繼承SuperType
淆游,只需要讓前者的prototype指向后者的實(shí)例就行了。
function SuperType() {
this.name = 'Yvette';
this.colors = ['pink', 'blue', 'green'];
}
SuperType.prototype.getName = function () {
return this.name;
}
function SubType() {
this.age = 22;
}
SubType.prototype = new SuperType();
SubType.prototype.getAge = function() {
return this.age;
}
SubType.prototype.constructor = SubType;
let instance1 = new SubType();
instance1.colors.push('yellow');
console.log(instance1.getName()); //'Yvette'
console.log(instance1.colors);//[ 'pink', 'blue', 'green', 'yellow' ]
let instance2 = new SubType();
console.log(instance2.colors);//[ 'pink', 'blue', 'green', 'yellow' ] 隔盛,注意這里instance2的屬性和instanse1共享了
此時(shí)我們不僅可以繼承父類的屬性犹菱,函數(shù)也獲得了繼承。
但是這樣實(shí)現(xiàn)繼承的缺陷在于——所有實(shí)例的屬性都共享了吮炕。這顯然也是不可接受的腊脱,我們想要每個(gè)實(shí)例能有自己的屬性,只用繼承同樣的函數(shù)就行了龙亲。此時(shí)我們會(huì)想到:借用構(gòu)造函數(shù)的繼承可以使每個(gè)實(shí)例擁有自己的屬性陕凹,而原型鏈繼承可以繼承父類的函數(shù),能不能把二者的優(yōu)點(diǎn)集合起來(lái)呢鳄炉?
三杜耙、組合繼承(借用構(gòu)造函數(shù) + 原型鏈繼承)
我們可以使用構(gòu)造函數(shù)來(lái)實(shí)現(xiàn)對(duì)屬性的繼承,再用原型鏈實(shí)現(xiàn)對(duì)方法的繼承拂盯,綜合二者各自的優(yōu)點(diǎn)就能實(shí)現(xiàn)一個(gè)功能完善的繼承了佑女。
function SuperType(name) {
this.name = name;
this.colors = ['pink', 'blue', 'green'];
}
SuperType.prototype.sayName = function () {
console.log(this.name);
}
function SuberType(name, age) {
SuperType.call(this, name);
this.age = age;
}
SuberType.prototype = new SuperType();
SuberType.prototype.constructor = SuberType;
SuberType.prototype.sayAge = function () {
console.log(this.age);
}
let instance1 = new SuberType('Yvette', 20);
instance1.colors.push('yellow');
console.log(instance1.colors); //[ 'pink', 'blue', 'green', 'yellow' ]
instance1.sayName(); //Yvette
let instance2 = new SuberType('Jack', 22);
console.log(instance2.colors); //[ 'pink', 'blue', 'green' ]
instance2.sayName();//Jack
如上所示,每個(gè)子類的實(shí)例都擁有了自己的屬性谈竿,并且都繼承了父類的sayName
方法(注意父類的方法是定義在prototype對(duì)象上的)团驱。
這個(gè)方案已經(jīng)基本能在實(shí)踐中使用了,但是它還是有一個(gè)小問(wèn)題——父類的構(gòu)造函數(shù)調(diào)用了2次空凸。一次是在子類構(gòu)造函數(shù)中調(diào)用父類構(gòu)造函數(shù)嚎花,另一次是在把子類的原型用父類的一個(gè)實(shí)例賦值時(shí)。理論上還是有優(yōu)化的空間呀洲。
現(xiàn)在我們?cè)賮?lái)看看另一種繼承的實(shí)現(xiàn)紊选,也是一種很有名的實(shí)現(xiàn)啼止。
四、原型式繼承
借助原型可以基于已有的對(duì)象創(chuàng)建新對(duì)象丛楚,不必因此創(chuàng)建新類型族壳。我們來(lái)看一個(gè)函數(shù):
function object(o) {
function F() { }
F.prototype = o;
return new F();
}
在object()
函數(shù)內(nèi)部創(chuàng)建了一個(gè)臨時(shí)類型F
,然后將傳入的對(duì)象作為它的原型并返回了一個(gè)實(shí)例。本質(zhì)上相當(dāng)于對(duì)傳入的對(duì)象進(jìn)行了一次淺拷貝趣些。實(shí)踐中我們不需要手寫這個(gè)函數(shù),而是可以直接使用Object.create()
來(lái)實(shí)現(xiàn)同樣的功能贰您。
在沒(méi)有必要?jiǎng)?chuàng)建單獨(dú)的構(gòu)造函數(shù)來(lái)實(shí)現(xiàn)一些定制功能坏平,只是需要讓兩個(gè)對(duì)象的行為保持一致時(shí),我們可以使用這樣的原型式繼承锦亦。
當(dāng)然它也具有原型鏈繼承的缺點(diǎn)舶替,無(wú)法為每個(gè)實(shí)例創(chuàng)建自己獨(dú)有的屬性。
五杠园、寄生式繼承
寄生式繼承是基于原型式繼承的顾瞪,只不過(guò)在創(chuàng)建對(duì)象的過(guò)程中以某種方式對(duì)它進(jìn)行了一些增強(qiáng)。
function createAnother(original) {
var clone = object(original);// 通過(guò)調(diào)用函數(shù)創(chuàng)建一個(gè)新對(duì)象
clone.sayHi = function () {// 以某種方式增強(qiáng)這個(gè)對(duì)象
console.log('hi');
};
return clone;// 返回這個(gè)對(duì)象
}
var person = {
name: 'Yvette',
hobbies: ['reading', 'photography']
};
var person2 = createAnother(person);
person2.sayHi(); //hi
但是依然沒(méi)有解決原型式繼承的問(wèn)題抛蚁。
根據(jù)前面提到的組合繼承的思路陈醒,我們?cè)僖淮嗡伎寄芊袷褂媒M合多種方案來(lái)解決問(wèn)題。
六瞧甩、寄生組合式繼承
通過(guò)名字我們大概能猜到钉跷,這種方式是組合了寄生式繼承、借用構(gòu)造函數(shù)的方式肚逸。
首先我們通過(guò)寄生式繼承實(shí)現(xiàn)方法的繼承:
function inherit(superType, subType) {
var o = object(superType.prototype);
o.constructor = subType; // 注意這一步——維持constructor是subType爷辙,因?yàn)樯弦恍袑rototype設(shè)為了superType
subType.prototype = o;
}
接著我們補(bǔ)充構(gòu)造函數(shù)來(lái)實(shí)現(xiàn)對(duì)實(shí)例屬性的繼承:
function SuperType(name) {
this.name = name;
this.colors = ['red', 'blue', 'yellow'];
}
SuperType.prototype.sayName = function () {
alert(this.name)
}
function SubType(name, age) {
SuperType.call(this, name); //只調(diào)用了一次構(gòu)造函數(shù)
this.age = age;
}
inherit(SuperType, SubType);
......
現(xiàn)在我們實(shí)現(xiàn)了一個(gè)較為完善的繼承:
它既能實(shí)現(xiàn)方法的復(fù)用,又能保證每個(gè)實(shí)例擁有各自的屬性朦促,同時(shí)它只調(diào)用了一次構(gòu)造函數(shù)膝晾,因?yàn)槲覀儧](méi)有像原型鏈繼承一樣創(chuàng)建額外的一個(gè)父類型的實(shí)例給子類型的原型。
這也就是我們實(shí)踐中可以使用的一種繼承的實(shí)現(xiàn)方式务冕。
ES 6的繼承
ES 6中新增了class和extends關(guān)鍵字血当,可以讓我們?cè)贘S中實(shí)現(xiàn)其他基于類的繼承的語(yǔ)言的繼承寫法。
class SubType extends SuperType {
}
當(dāng)然雖然我們可以用如此簡(jiǎn)潔的寫法完成繼承洒疚,實(shí)際上底層實(shí)現(xiàn)仍然是基于原型實(shí)現(xiàn)的歹颓,只不過(guò)Babel幫我們完成了這部分轉(zhuǎn)譯工作。而轉(zhuǎn)譯出的代碼實(shí)質(zhì)上和我們上面所寫的寄生組合式繼承的代碼是大同小異的油湖。