1. 程式人生 > >前端【JS】,深入理解原型和原型鏈

前端【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:我也是看這兩篇部落格理解的)
https://www.cnblogs.com/echolun/p/12321869.html;

https://www.cnblogs.com/echolun/p/12384935.html#4569574

&n