成為一名前端工程師差不多有一年了晶府,仍然對原型繼承的概念一知半解,期間查看網(wǎng)上許多資料钻趋,還是沒有建立對原型繼承建立起完全的知識鏈川陆。直到看了《JavaScript語言精粹與編程實踐》一書,才對原型繼承有了一個全面的認(rèn)識蛮位。在本書中作者提出的觀點顛覆了我對原型繼承的認(rèn)識较沪,所以在本篇文章我將對原型繼承的知識進(jìn)行歸納總結(jié)鳞绕,力求全面而又準(zhǔn)確的介紹原型繼承。
原型繼承是JavaScript最重要的語言特性之一尸曼,接下來將從對象形成過程和構(gòu)造過程詳細(xì)講解原型繼承们何。
空的對象是所有對象的基礎(chǔ)
在展開整個話題之前,我們必須要先從最基本的Object()構(gòu)造器開始控轿。
obj3 = Object.prototype;
var num = 0;
for(var i in obj){
num++;
}
console.log(num); //0
上面的代碼已經(jīng)明確的展示出Object()構(gòu)造器的原型obj3就是一個空的對象冤竹。可能有人對空的對象不是很了解茬射,所以鹦蠕,在這里簡單介紹下空的對象≡谂祝空的對象實質(zhì)上只是滿足以下條件的數(shù)據(jù)結(jié)構(gòu):
-
__proto__
屬性指向Object.prototype
- 其成員列表指向一個空殼
所謂的“空的對象”就是一個標(biāo)準(zhǔn)的通過Object()
構(gòu)造出來的對象實例钟病。下面例子中的obj1和obj2都是空的對象。
obj1 = new Object();
obj2 = {};
現(xiàn)在刚梭,讓我們回到Object()構(gòu)造器的原型肠阱,我們說到obj3是一個空的對象,而obj1和obj2也是一個空的對象朴读,那么是不是可以認(rèn)為對象的形成就是一個簡單的復(fù)制過程屹徘,就如同下面這張圖片描述一樣。
但是如果真的如上面這張途中所描述的那種磨德,每次構(gòu)造一個實例都從原型中復(fù)制出一個實例來的話缘回,那么內(nèi)存的占用的消耗會急速增加,這樣會對性能產(chǎn)生影響典挑。所以JavaScript僅當(dāng)寫某個實例的成員時酥宴,將成員的信息復(fù)制到實例的映像中,也就是當(dāng)需要寫對象屬性的時候(obj12.value=10)您觉,會產(chǎn)生一個名為value的屬性值拙寡,放在obj2對象的成員列表中。所以相對于obj1琳水,obj2就多出了一張成員列表肆糕,但是obj2依舊是一個指向原型的引用,下面這張圖很清楚的描述了obj2對象在孝。
在此補(bǔ)充一個小知識點诚啃,對象的成員的存取遵循兩個規(guī)則:1、在讀取時私沮,該成員表上的屬性和方法將會被優(yōu)先訪問到始赎;2、如果成員表中沒有指定的屬性,那么會查詢對象的整個原型鏈造垛,直到找到該屬性或者原型鏈的頂部魔招。所以存取實例中的屬性比存取原型中的屬性效率要高。
在這里需要特別指出的是:Object.prototype處于原型鏈的頂部(盡管有些文章認(rèn)為null處于原型鏈的頂端五辽,因為事實上Object.prototype.__proto__===null
,但是null一無所有办斑,其跟空的對象不同,空的對象里面還有預(yù)定義的屬性和方法杆逗,而null里面就是空的乡翅,其是一個特殊的對象,從原型繼承的角度上看髓迎,對象根本不可能從null中繼承到什么峦朗,所以null并不是原型鏈的頂部,而Object.prototype才被認(rèn)為處于原型鏈的頂部)排龄,因此所有的對象均從Object.prototype中繼承預(yù)定義的屬性和方法波势,而不是從null中繼承,所以空的對象才可以是所有對象的基礎(chǔ)橄维。
從函數(shù)到構(gòu)造器
在上面我們分析了對象的形成過程尺铣,并沒有解釋函數(shù)作為構(gòu)造函數(shù)到底發(fā)生了什么,所以在這將分析函數(shù)到構(gòu)造器的構(gòu)造過程争舞。同上面對象的形成一樣凛忿,如果每次聲明一個函數(shù)的時候,都會先創(chuàng)建一個對象實例竞川,之后將函數(shù)中的prototype成員指向該對象實例店溢,那將是非常不經(jīng)濟(jì)。那么JavaScript是怎么操作的委乌?
在JavaScript內(nèi)部床牧,構(gòu)造函數(shù)跟普通函數(shù)并沒有區(qū)別,二者在聲明時遭贸,其prototype值是null戈咳,只有在需要引用到原型的時候(即通過new關(guān)鍵字調(diào)用進(jìn)行創(chuàng)建對象實例),才具有構(gòu)造器的特性壕吹,所以在JavaScript內(nèi)部的實現(xiàn)很有可能是如下代碼所示:
//設(shè)定__proto__是函數(shù)內(nèi)置的方法著蛙,get_prototype()是它的讀方法
var __proto__ = null;
function get_prototype(){
if(!__proto__){
__proto__ = new Object();
__proto__.constructor = this;
};
return __proto__;
};
在上面的代碼可以清晰的發(fā)現(xiàn)函數(shù)的原型是一個標(biāo)準(zhǔn)的、系統(tǒng)內(nèi)置的Object()構(gòu)造器的一個實例耳贬,當(dāng)該實例構(gòu)建完成后其constructor屬性被賦值為當(dāng)前函數(shù)踏堡。這點非常好證明,因為可以使用delete運算符刪去當(dāng)前的屬性咒劲,讓成員取到父類的屬性值暂吉。
function MyObject(){
};
console.log(MyObject.prototype.constructor == MyObject); //true
delete MyObject.prototype.constructor;
console.log(MyObject.prototype.constructor == Object); //true
console.log(MyObject.prototype.constructor == new Object().constructor);// true
所有MyObject.prototype實際上與一個普通對象并沒有本質(zhì)的區(qū)別胖秒,當(dāng)一個函數(shù)的prototype有意義之后缎患,其就變成了一個“構(gòu)造器”慕的。事實上,我們可以假設(shè)構(gòu)造器的prototype屬性總是來自于new Object()產(chǎn)生的實例挤渔,這個假設(shè)對我們以后理解“動態(tài)語言特性”有非常大的幫助肮街。
從構(gòu)造過程,我們知道了JavaScript的實例對象實際上是一個指向其原型的判导,并持有一個成員列表的結(jié)構(gòu)嫉父。聯(lián)想到之前空的對象的實質(zhì),我們不難推斷出:所有的實例對象的共同原型Object.prototype具有某些性質(zhì)眼刃,而使這些實例對象具有對象的某些性質(zhì)绕辖。
如果對對象實例的性質(zhì)做一個分類的話,可以劃分成:
成員名 | 類型 | 分類 |
---|---|---|
tostring | function | 動態(tài)語言 |
toLocaleString | function | 動態(tài)語言 |
valueOf | function | 動態(tài)語言 |
constructor | function | 對象系統(tǒng):構(gòu)造 |
propertyIsEnumerable | function | 對象系統(tǒng):屬性 |
hasOwnProperty | function | 對象系統(tǒng):屬性 |
isPrototypeOf | function | 對象系統(tǒng):原型 |
但是對于一個構(gòu)造函數(shù)而言擂红,其還具有一些特殊的屬性仪际,那就是原型prototype
。
原型鏈的維護(hù)
從構(gòu)造器的特殊屬性prototype
來看昵骤,似乎只有構(gòu)造函數(shù)才能維護(hù)原型鏈树碱。從之前的討論來看,一個實例對象應(yīng)該擁有一個指向原型的__proto__
屬性(即實例對象的__proto__
指向構(gòu)造其的構(gòu)造函數(shù)的prototype
)变秦,該屬性是不可見的成榜,但是在某些瀏覽器中以__proto__
的形式將該屬性暴露出來供開發(fā)者調(diào)試,該屬性也被稱之為“內(nèi)部原型鏈”(該原型鏈才是真正的原型鏈)蹦玫,其與構(gòu)造函數(shù)的prototype
所組成的“構(gòu)造器原型鏈”共同組成JavaScript的原型鏈赎婚。下面來看一個示例:
function MyObject(){
};
function MyObjectEx(){
};
//構(gòu)造器原型鏈
MyObjectEx.prototype = new MyObject();
var obj1 = new MyObjectEx();
var obj2 = new MyObjectEx();
下圖展示了代碼所構(gòu)成的內(nèi)部原型鏈與構(gòu)造原型鏈:
該圖構(gòu)造器通過ptototype
構(gòu)建了一個原型鏈,對象實例通過__proto__
構(gòu)建了一個原型鏈樱溉,但是由于__proto__
不可訪問挣输,所以沒有辦法從對象實例開始訪問整個原型鏈。這時候饺窿,constructor
就閃亮登場了歧焦。
constructor
function MyObject(){
};
var obj1 = new MyObject();
//構(gòu)造函數(shù)的原型的constructor屬性指向了構(gòu)造器本身
console.log(MyObject.prototype.constructor===MyObject);//true
//在JS中,一個實例對象的constructor屬性總是指向構(gòu)造函數(shù)肚医。
console.log(obj.construcrot===MyObject);//true
所以绢馍,經(jīng)過上面的代碼,我們發(fā)現(xiàn)了一個連接點肠套,通過constructor可以讓實例對象訪問構(gòu)造函數(shù)的原型鏈舰涌。經(jīng)過了constructor的連接,這個原型鏈的連接就變成如下所示:
既然我們只需要使用正確的constructor屬性就可以使用整個原型鏈你稚,那么其內(nèi)部原型鏈有什么用瓷耙。這就要提到原型繼承的實質(zhì)了朱躺。面向?qū)ο蟮睦^承性明確約定了:子類與父類具有相似性。在原型繼承中搁痛,相似性是在構(gòu)造的時候決定的长搀,也是由new運算內(nèi)部的那個“復(fù)制”操作決定的。所以__proto__
屬性就是為了保證這種一致性鸡典,也可以這樣說內(nèi)部原型鏈?zhǔn)荍avaScript的原型繼承機(jī)制所決定源请。
所以在這里簡單總結(jié)下構(gòu)造函數(shù)、原型彻况、隱式原型和實例的關(guān)系:每個構(gòu)造函數(shù)都有一個原型屬性(prototype
)谁尸,該屬性指向構(gòu)造函數(shù)的原型對象;而實例對象有一個隱式原型屬性(__proto__
)纽甘,其指向構(gòu)造函數(shù)的原型對象(obj.__proto__==Object.prototype
)良蛮;同時實例對象的原型對象中有一個constructor
屬性,其指向構(gòu)造函數(shù)悍赢。(該關(guān)系用文字描述有點干澀决瞳,所以可能會在下篇文章中說明這些問題)