前端【JS】,深入理解原型和原型鏈
- 2020 年 5 月 8 日
- 筆記
- 【前端】javascript
對於原型和原型鏈,相信有很多夥伴都說的上來一些,但有具體講不清楚。但面試的時候又經常會碰到面試官的死亡的追問,我們慢慢來梳理這方面的知識!
要理解原型和原型鏈的關係,我們首先需要了解幾個概念;
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
其實到這裡,我們已經解釋了 構造函數與普通函數有什麼區別
這個問題,構造函數其實就是一個普通函數,且函數都支援new調用與普通調用。也正因如此導致了ES5中構造函數沒有區別於普通函數的尷尬局面,這也是為何在ES6中JavaScript正式推出Class類的原因,你會發現Class只支援new調用,如果直接調用會報錯:
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:我也是看這兩篇部落格理解的)
//www.cnblogs.com/echolun/p/12321869.html;
//www.cnblogs.com/echolun/p/12384935.html#4569574