前端【JS】,深入理解原型和原型鏈
對於原型和原型鏈,相信有很多夥伴都說的上來一些,但有具體講不清楚。但面試的時候又經常會碰到面試官的死亡的追問,我們慢慢來梳理這方面的知識!
要理解原型和原型鏈的關係,我們首先需要了解幾個概念;
1、什麼是建構函式?
2、建構函式與普通函式有什麼區別?
3、原型鏈的頂端是什麼?
4、prototype、__proto__、constructor在什麼物件下存在?
OK 我們暫時帶著這些疑問往下看;
一、什麼是建構函式?建構函式與普通函式有什麼區別?
建構函式其實就是一個普通函式,只是我們為了區分普通函式,通常建議建構函式name首字母大寫;
// 這是一個建構函式 function Parent(){};
你說我就不首字母大寫,那也不影響一個函式是建構函式的事實:
// 這也是一個建構函式 function parent(){ this.name = '不禿頭'; }; let child = new parent(); console.log(child);//parent {name: "不禿頭"}
有同學就納悶了,這普通函式居然也能使用new操作符構造呼叫,沒錯,不僅普通函式能new呼叫,建構函式同樣也能普通呼叫:
// 這是一個建構函式 function Parent() { console.log(1); }; Parent() //1
其實到這裡,我們已經解釋了 建構函式與普通函式有什麼區別
class Parent { sayName() { console.log('不禿頭'); }; }; var child = new Parent(); child.sayName(); //不禿頭 var child = Parent();//報錯,必須使用new呼叫
解釋了建構函式,那麼建構函式能用來做什麼呢?最基本的就是屬性繼承了,我們先不聊繼承模式,就從最基本的繼承說起。
假設現在我們要定製一批藍色的杯子,杯口直徑與高度可互不相同,那麼我們可以用建構函式表示:
//定製杯子 function CupCustom(diameter, height) { this.diameter = diameter; this.height = height; }; CupCustom.prototype.color = 'blue'; var cup1 = new CupCustom(8, 15); var cup2 = new CupCustom(5, 10); console.log(cup1.height);//15 console.log(cup2.color);//blue
那麼我們可以將建構函式CupCustom理解成一個製作杯子的模具,cup1與cup2是模具製作出來的杯子,我們稱之為例項。大家可以嘗試輸出例項,可以看到兩個例項都繼承了建構函式的構造器屬性(直徑,高)與原型屬性(顏色),顏色存放的地方還有點不同,它放在__proto__
中,說到這咱們解釋了為什麼例項能讀取height與color兩個屬性。
出於好奇,咱們也輸出列印了建構函式的屬性,有同學不知道怎麼列印檢視函式的屬性,這裡可以借用console.dir(函式)
,列印結果如下圖:
對比圖1與圖2可以發現,建構函式除了自身屬性與__proto__
屬性外還多出了一個prototype
屬性,這裡我們其實能先給出一個結論:
所有的物件都有__proto__
屬性,但只有函式擁有prototype
屬性;
二、prototype與__proto__
"萬物皆物件",這句話我想不止前端的同學,應該搞開發的同學都聽過吧。
我們知道JavaScript中資料型別分類基本資料型別與引用資料型別:
- 基本資料型別:Number,String,Boolean,Undefined,Null,Symbol。
- 引用資料型別:Object,Function,Date,Array,RegExp等。
不知道大家有沒有想過這樣一件事,為什麼隨便宣告一段字串就能使用字串的方法?如果字串真的就是簡單型別,方法又是從哪來的呢?
經實驗,在這些型別中,基本型別中除了undefined與null之外,任意數字,字元,布林以及symbol值都有__proto__
屬性,以字串為例,我們列印它的__ptoto__
並展開,如下可以看到大量我們日常使用的字串方法均在其中:
所有的物件都有__ptoto__
屬性,而字串居然也有__proto__
屬性,__proto__
是一個訪問器屬性,它指向建立它的建構函式的原型prototype。還記得前面做杯子的建構函式嗎?每例項個杯子其實只有直徑與高度屬性,但通過例項的__proto__
屬性我們找到了建構函式CupCustom的原型prototype,從而成功訪問了prototype上的color屬性。
prototype:是函式的一個屬性(每個函式都有一個prototype屬性),這個屬性是一個指標,指向一個物件。它是顯示修改物件的原型的屬性。
__proto__:是一個物件擁有的內建屬性(請注意:prototype是函式的內建屬性,__proto__是物件的內建屬性),是JS內部使用尋找原型鏈的屬性。
那為什麼函式的prototype屬性下還有一個__proto__
屬性呢?
我們知道函式有函式表示式,函式宣告以及new建立三種模式,而函式宣告其實等同於new Function()
,我們定義的任意函式本質上也屬於原始建構函式Function
的例項,那麼函式有一個__proto__
屬性指向建構函式Function
的原型不是理所應當的事情麼。所以這裡我們又得出了一個結論:
每一個函式都屬於原始建構函式
Function
的例項,而每一個函式又能做為建構函式生產屬於自己的例項。
三、關於prototype
上面已經知道。prototype是函式特有的屬性,__proto__是每個物件都有的屬性;所以函式物件下面有兩個屬性,下圖1,而不是函式物件就只有一個__proto__屬性(例項化的物件)下圖2;
每個物件都有__proto__
屬性,物件都能通過此屬性找到建立自己建構函式的原型。那麼什麼是原型呢?原型其實就是一個物件。
上圖3中,prototype下面有兩個屬性:__proto__和constructor,constructor它指向建立它的建構函式,
例項的__proto__
指向的是建立自己的建構函式的prototype,這個prototype是一個物件;實驗是檢驗真的唯一標準;
a.__proto__ === Foo.prototype // true 說明:例項化的物件的__proto__ 恆等於建構函式的原型物件prototype;
讓我們來用圖形轉化來表達;
通過這個圖我們就把上面所說的都總結了;
例項物件的__proto__ 指向建構函式的原型prototype;
建構函式原型物件下面的constructor指向建立自己的建構函式;
我們補充一點知識:
數字 123 本質上由建構函式Number()
建立,所以數字123通過__proto__
訪問建構函式Number()
原型上的方法屬性。
字串 abc 本質上由建構函式 String()
建立,所以abc也能通過__proto__
訪問建構函式String()
原型上的方法屬性。
函式本質上由原始建構函式Function
建立,所以函式也能通過__proto__
訪問原始建構函式Function
上的原型屬性方法,別忘了,我們任意建立的函式都能使用call、apply等方法,不然你以為這些方法是哪來的呢。
上文也說了,我們自己建立建構函式其實和普通函式沒任何區別,畢竟每個函式都能使用new呼叫用於建立屬於自己的例項,這種繼承方式是不是神似java的類,只是在JavaScript中改用原型prototype了。每一個函式都有作為建構函式的潛力,所以每一個函式都自帶了prototype原型。
原始建構函式Function()
扮演著創世主女媧的角色,她創造了Object()、Number()、String()、Date()、function fn(){}等第一批人類(也就是建構函式),而人類同樣具備了繁衍的能力(使用new操作符),於是Number()繁衍出了資料型別資料,String()誕生了字串,function fn(){}作為建構函式也誕生了各種各樣的物件後代。
我們通過程式碼證實這一點:
// 所有函式物件的__proto__都指向Function.prototype,包括Function本身 Number.__proto__ === Function.prototype //true Number.constructor === Function //true String.__proto__ === Function.prototype //true String.constructor === Function //true Object.__proto__ === Function.prototype //true Object.constructor === Function //true Array.__proto__ === Function.prototype //true Array.constructor === Function //true Function.__proto__ === Function.prototype //true Function.constructor === Function //true
所以當例項訪問某個屬性時,會先查詢自己有沒有,如果沒有就通過__proto__
訪問自己建構函式的prototype有沒有,前面說建構函式的原型是一個物件,如果原型物件也沒有,就繼續順著建構函式prototype中的__proto__
繼續查詢到建構函式Object()的原型,再看有沒有,如果還沒有,就返回undefined,因為再往上就是null了,這個過程就是我們熟知的原型鏈,說的再準確點,就是__proto__
訪問過程構成了原型鏈;
那物件可以一直__proto__往下找嗎?答案是否定的。例項通過訪問器屬性__proto__
訪問建立自己的建構函式原型,相等是很正常的。原型下面的prototype.__proto__返回的是一個物件建構函式的原型Object.prototype,因為prototype是一個物件,物件的建構函式指向的是Object,Object.prototype.__proto__就是原型鏈的頂端null;上程式碼,根據下面程式碼就能理解原型和原型鏈的關係了;
function Parent() {}; var son = new Parent(); console.log(son.__proto__); //找到了建構函式Parent的原型 console.log(son.__proto__.__proto__); //原型是物件,它的__proto__指向建構函式Object的原型 console.log(son.__proto__.__proto__.__proto__); //null,到頭了,null不是物件,沒有原型,所以不會繼續往上了
總結: 這篇文章寫起來說實話我的思路有點亂,但在最後面這張圖如果你能理解的話,說明你已經對原型和原型鏈已經理解了,貌似好像知道了什麼是原型和原型鏈,工作上用的地方好像不多,有一說一,確實~;但它並不影響 我們加深對函式的認識和理解,而且前端面試的時候,這百分之八九十都會問的原型和原型鏈,如果你理解了的話,相信你就能在面試的過程中迎刃有餘;
歡迎大家一起討論和指導;謝謝大家!
如果我的部落格思路不夠清晰的話,推薦大家看下這兩篇部落格:(ps:我也是看這兩篇部落格理解的)
https://www.cnblogs.com/echolun/p/12321869.html;
https://www.cnblogs.com/echolun/p/12384935.html#4569574
&n