為什么不能在原型鏈上使用對象?以及JS原型鏈的深層原理是什么?
在剛剛接觸JS原型鏈的時候都會接觸到一個熟悉的名詞:prototype
;如果你曾經深入過prototype
,你會接觸到另一個名詞:__proto__
(注意:兩邊各有兩條下劃線,不是一條)。以下將會圍繞prototype
和__proto__
這兩個名詞解釋
一、為什么不能在原型鏈上使用對象:
先舉一個非常簡單的例子,我有一個類叫Humans(人類),然后我有一個對象叫Tom(一個人)和另一個對象叫Merry(另一個人),很明顯Tom和Merry都是由Humans這一個類實例化之后得到的,然后可以把這個例子寫成如下代碼:
function Humans() { this.foot = 2; } Humans.prototype.ability = true;var Tom = new Humans();var Merry = new Humans(); console.log(Tom.foot);//結果:2console.log(Tom.ability);//結果:trueconsole.log(Merry.foot);//結果:2console.log(Merry.ability);//結果:true
以上是一個非常簡單的面向對象的例子,相信都能看懂,如果嘗試修改Tom的屬性ability,則
function Humans() { this.foot = 2; } Humans.prototype.ability = true;var Tom = new Humans();var Merry = new Humans(); Tom.ability = false; console.log(Tom.foot);//結果:2console.log(Tom.ability);//結果:falseconsole.log(Merry.foot);//結果:2console.log(Merry.ability);//結果:true
以上可以看出Tom的ability屬性的值改變了,但并不影響Merry的ability屬性的值,這正是我們想要的結果,也是面向對象的好處,由同一個類實例化得到的各個對象之間是互不干擾的;OK,接下來給ability換成object對象又如何?代碼如下:
function Humans() { this.foot = 2; } Humans.prototype.ability = { run : '100米/10秒', jump : '3米'};var Tom = new Humans();var Merry = new Humans(); Tom.ability = { run : '50米/10秒', jump : '2米'};console.log(Tom.ability.run); //結果:'50米/10秒'console.log(Tom.ability.jump); //結果:'2米'console.log(Merry.ability.run); //結果:'100米/10秒'console.log(Merry.ability.jump); //結果:'3米'
以上代碼就是在原型鏈上使用了對象,但從以上代碼可以看出Tom的ability屬性的改變依然絲毫不會影響Merry的ability的屬性,于是乎你會覺得這樣的做法并無不妥,為什么說不能在原型鏈上使用對象?接下來的代碼就會顯得很不一樣,并且可以完全表達出原型鏈上使用對象的危險性:
function Humans() { this.foot = 2; } Humans.prototype.ability = { run : '100米/10秒', jump : '3米'};var Tom = new Humans();var Merry = new Humans(); Tom.ability.run = '50米/10秒'; Tom.ability.jump = '2米';console.log(Tom.ability.run); //結果:'50米/10秒'console.log(Tom.ability.jump); //結果:'2米'console.log(Merry.ability.run); //結果:'50米/10秒'console.log(Merry.ability.jump); //結果:'2米'
沒錯,從以上代碼的輸出結果可以看出Tom的ability屬性的改變影響到Merry的ability屬性了,于是就可以明白在原型鏈上使用對象是非常危險的,很容易會打破實例化對象之間的相互獨立性,這就是為什么不能在原型鏈上使用對象的原因?是的,但我想說的可不只如此,而是其中的原理,看完后面JS原型鏈的深層原理之后,相信你會完全明白。
在以下第二部份解釋JS原型鏈的深層原理之前,先來明確一個概念:原型鏈上的屬性或方法都是被實例化對象共用的,正因如此,上面的Tom.ability.run=’50米/10秒’,改動了原型連上的ability才導致另一個對象Merry受影響,既然如此,你可能會問Tom.ability = {……}不也是改動了原型鏈上的ability嗎,為什么Merry沒有受影響?答案是Tom.ability = {……}并沒有改動原型鏈上的ability屬性,而是為Tom添加了一個自有屬性ability,以后訪問Tom.ability的時候不再需要訪問原型鏈上的ability,而是訪問其自有屬性ability,這是就近原則;OK,如果你仍有疑問,可以用紙筆記下你的疑問,繼續往下看你會更加明白。
二、JS原型鏈的深層原理:
首先要引入一個名詞__proto__
,__proto__
是什么?在我的理解里,__proto__
才是真正的原型鏈,prototype
只是一個殼。如果你使用的是chrome瀏覽器,那么你可以嘗試使用console.log(Tom.__proto__
.ability.run),你發現這樣的寫法完全可行,而且事實上當只有原型鏈上存在ability屬性的時候,Tom.ability其實是指向Tom.__proto__
.ability的;當然,如果你跑到IE瀏覽器里嘗試必然會報錯,事實上IE瀏覽器禁止了對__proto__
的訪問,而chrome則是允許的,當然實際開發中,我并不建議直接就使用__proto__
這一屬性,但它往往在我們調試代碼時發揮著重要作用。有人可能會問到底Tom.__proto__
和Humans.prototype
是什么關系,為了理清兩者的關系,下面先列出三條法則:
1、對象是擁有__proto__
屬性的,但沒有prototype
;例如:有Tom.__proto__
,但沒有Tom.prototype
。
2、類沒有__proto__
屬性,但有prototype
;例如:沒有Humans.__proto__
,但有Humans.prototype
(這里必須糾正一下,同時非常感謝‘川川哥哥’提出這一處錯處,確實是我在寫到這一點的時候沒有考慮清楚,事實上Humans也是Function的一個實例對象,因此Humans.__proto__
===Function.prototype
是絕對成立的,稍有特殊的是這時Function.prototype是指向一個Empty(空)函數,值得推敲)。
3、由同一個類實例化(new)得到的對象的__proto__
是引用該類的prototype
的(也就是我們說的引用傳遞);例如Tom和Merry的__proto__
都引用自Humans的prototype
。
OK,上面說過Tom.ability={……}其實并沒有改變原型鏈上的ability屬性,或者說并沒有改變Tom.__proto__
.ability,而是為Tom添加了一個自有的ability屬性,為了說明這一點,我們再次回到以上的第三個代碼塊,其代碼如下:
function Humans() { this.foot = 2; } Humans.prototype.ability = { run : '100米/10秒', jump : '3米'};var Tom = new Humans();var Merry = new Humans(); Tom.ability = { run : '50米/10秒', jump : '2米'};console.log(Tom.ability.run); //結果:'50米/10秒'console.log(Tom.ability.jump); //結果:'2米'console.log(Merry.ability.run); //結果:'100米/10秒'console.log(Merry.ability.jump); //結果:'3米'
當為Tom.ability賦予新的值后,再次訪問Tom.ability時就不再指向Tom.__proto__
.ability了,因為這時其實是為Tom添加了自有屬性ability,可以就近取值了,你可以嘗試用Chrome瀏覽器分別console.log(Tom.ability.run)和console.log(Tom.__proto__
.ability.run),你會發現確實存在兩個不同的值,再看完下面的圖后,相信你會完全明白: 于是可以有這樣一個結論:當訪問一個對象的屬性或方法的時候,如果對象本身有這樣一個屬性或方法就會取其自身的屬性或方法,否則會嘗試到原型鏈(
__proto__
)上尋找同名的屬性或方法。明白了這一點后,要解釋以上第四個代碼塊的原理也非常容易了,其代碼如下:
function Humans() { this.foot = 2; } Humans.prototype.ability = { run : '100米/10秒', jump : '3米'};var Tom = new Humans();var Merry = new Humans(); Tom.ability.run = '50米/10秒'; Tom.ability.jump = '2米';console.log(Tom.ability.run); //結果:'50米/10秒'console.log(Tom.ability.jump); //結果:'2米'console.log(Merry.ability.run); //結果:'50米/10秒'console.log(Merry.ability.jump); //結果:'2米'
當Tom.ability.run=’50米/10秒’的時候,JS引擎會認為Tom.ability是存在的,因為有Tom.ability才會有Tom.ability.run,所以引擎開始尋找ability屬性,首先是會從Tom的自有屬性里尋找,在自有屬性里并沒有找到,于是到原型鏈里找,結果找到了,于是Tom.ability就指向了Tom.__proto__
.ability了,修改Tom.ability.run的時候實際上就是修改了原型鏈上的ability了,因而影響到了所有由Humans實例化得到的對象,如下圖:
希望上面所講的內容足夠清楚明白,下面通過類的繼承對原型鏈作更進一步的深入:
先來看一個類的繼承的例子,代碼如下:
function Person() { this.hand = 2; this.foot = 2; } Person.prototype.say = function () { console.log('hello'); }function Man() { Person.apply(this, arguments);//對象冒充 this.head = 1; } Man.prototype = new Person();//原型鏈Man.prototype.run = function () { console.log('I am running'); }; Man.prototype.say = function () { console.log('good byte'); }var man1 = new Man();
以上代碼是使用對象冒充和原型鏈相結合的混合方法實現類的繼承,也是目前JS主流的實現類的繼承的方法,如果對這種繼承方法缺乏了解,可以看看這里。
接下來看看以上實現繼承后的原型鏈,可以運用prototype
和__proto__
來解釋其中的原理:
1、從man1 = new Man(),可以知道man1的__proto__
是指向Man.prototype的,于是有:
公式一:man1.__proto__
=== Man.prototype 為true
2、從上面的代碼原型鏈繼承里面看到這一句代碼 Man.prototype = new Person(),作一個轉換,變成:Man.prototype = a,a = new Perosn();一個等式變成了兩個等式,于是由a = new Perosn()可以推導出a.__proto__
= Person.prototype,結合Man.prototype = a,于是可以得到:
公式二:Man.prototype.__proto__
=== Person.prototype 為true
由公式一和公式二我們就得出了以下結論:
公式三:man1.__proto__
.__proto__
=== Person.prototype 為true
公式三就是上述代碼的原型鏈,有興趣的話,可以嘗試去推導多重繼承的原型鏈,繼承得越多,你會得到一個越長的原型鏈,而這就是原型鏈的深層原理;從公式三可以得出一個結論:當你訪問一個對象的屬性或方法時,會首先在自有屬性尋找(man1),如果沒有則到原型鏈找,如果在鏈上的第一環(第一個__proto__
)沒找到,則到下一環找(下一個__proto__
),直到找到為止,如果到了原型鏈的盡頭仍沒找到則返回undefined(這里必須補充一點:同時非常感謝深藍色夢想提出的疑問:盡頭不是到了Object嗎?是的,原型鏈的盡頭就是Object,如果想問為什么,不妨做一個小小的實驗:如果指定Object.prototype.saySorry = ‘I am sorry’,那么你會驚喜地發現console.log(man1.saySorry)是會彈出結果‘I am sorry’的)。
聲明:本網頁內容旨在傳播知識,若有侵權等問題請及時與本網聯系,我們將在第一時間刪除處理。TEL:177 7030 7066 E-MAIL:11247931@qq.com