前端【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

其實到這裡,我們已經解釋了 構造函數與普通函數有什麼區別 這個問題,構造函數其實就是一個普通函數,且函數都支援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