一張圖帶你搞懂Javascript原型鏈關係
- 2021 年 8 月 9 日
- 筆記
- ES6系統學習, javascript, 本周面試題, 面試題
在某天,我聽了一個老師的公開課,一張圖搞懂了原型鏈。
老師花兩天時間理解、整理的,他講了兩個小時我們當時就聽懂了。
今天我把他整理出來,分享給大家。也讓我自己鞏固加深一下。
就是這張圖:

為了更好的圖文對照,我為每條線編了標號,接下來的細節講解,都會用到這張圖裡的編號:
為了你更好的對照閱讀,你可以單獨打開這張圖片,然後對比著文章看。
當然,我後邊也會貼心的把對應區域截小圖貼在文案附近。

前置知識
在對這張圖進行詳細拆解前,我們先來說幾個前置的基礎知識。以便後續更好的理解。
-
Function、Object、Array、String、Symbol等這些都是JavaScript的內建函數,也叫原生函數(js創造時,他們就存在的,是js內部提供的) -
prototype:原型對象; -
__proto__:隱式原型、對象的私有屬性; -
所有的函數都是Function構造出來的,包括Object等原生函數。可以說,每個函數都是Function類型的實例。 -
函數實際上是對象,但比較特殊,我們叫做函數對象 -
每個函數被創造出來時都有一個prototype,表示該函數的原型。他就是原型對象 -
每個對象身上都有一個私有屬性__proto__,指向該對象的構造函數的原型對象。函數作為對象也有__proto__ -
prototype是一個對象,由Object構造出來的。所以他身上也有__proto__,永遠指向對象的構造函數Object的原型(即:Object.prototype) -
函數都是被Function構造出來的,所以每個函數的__proto__都指向Function的原型(即:Function.prototype) -
Object.prototype的__proto__不能再指向自身無限循環,所以指向null -
Function.__proto__指向自身原型。因為Function沒人構造,「生下來」就有。
如下圖:
函數a,既有prototype、也有__proto__

內建函數Object,既有prototype、也有__proto__

對象身上就只有__proto__

口訣提煉
為了更好的掌握,我把相關的知識點匯總成下列幾條口訣。接下來的剖析中都會用到。
-
函數是Function構造出來的 -
一切函數都是對象 -
只要是函數對象,就會有原型prototype和隱式原型__proto__兩個屬性。 -
普通對象身上只有__proto__,沒有prototype -
實例化對象的__proto__都指向構造函數的prototype -
所有函數的prototype都指向自身prototype -
所有prototype的__proto__都指向Object.prototype(Object的除外) -
所有函數對象的__proto__都指向Function.prototype(包括Function自身) -
對象身上都有constructor指向函數自身
注意:這裡不考慮原型鏈指向修改、Object.create(null)這些特殊情況
剖析一張圖
接下來我們根據基礎知識和口訣,正式來看圖中的每一個細節
圖例:
觀察一個圖之前,我們先看他的圖例

右邊表示節點的類型:
綠色方塊:表示普通對象,比如平時創建的對象obj {}、arr **[]**等
紅色方塊:表示函數對象,也就是函數。他是一種特殊的對象。
左邊表示箭頭的指向:
綠色箭頭:表示用 new + 構造函數調用 的方式創建實例化對象
白色箭頭:表示當前節點的prototype原型對象的指向
藍色箭頭:表示當前節點的__proto__私有屬性的指向
詳情
Function
我們先看最右邊的Function(圖中色塊1)。
他是js的內部函數,你列印Function會得到標示著「本地程式碼」的結果。

也就是說他是js一開始就有的。
而伴隨他出生的就是他的原型: Function.prototype。(圖中色塊2)
prototype是函數特有的標誌,每個函數被創建出來,身上就有一個prototype的屬性,表示自己的原型對象。
根據口訣:所有函數的prototype都指向自身prototype。
也就是說,Function.prototype指向Function原型。
所以 Function.prototype === Function.prototype(圖中線條a)

然後說下比較特殊的Function.__proto__
因為Function他是個函數,函數又是一種特殊的對象(函數類對象,又叫函數對象)。所以作為對象身上特有的標誌__proto__,在Function身上也有一個。
另外,任何對象都可以理解為實例化對象,所以我們總結出口訣:實例化對象的__proto__都指向構造函數的prototype、所有prototype的__proto__都指向Object.prototype(Object的除外)
如:
const obj = new Object() // 或 const obj = {} 的字面量寫法
obj.proto === Object.prototype // true
// 實例化對象obj,其隱式原型proto指向構造函數Object的原型
所以,Function.__proto__本來也應該指向Function的構造函數的原型。
但是因為Function比較特殊,他是祖宗級別的函數,是JS中萬物開天闢地就有的,不能說誰把他構造出來的,
因此Function的__proto__的指向就比較特殊,他沒有自己的構造函數,於是就指向了自己的原型。
於是Function.__proto__指向自己的原型Function.prototype。
所以 Function.__proto__ === Function.prototype(圖中線條b)

這是原型鏈中第一個特殊點。
口訣:所有函數對象的__proto__都指向Function.prototype(包括Function自身)
擴展:
原型對象prototype身上都有constructor屬性,指回構造函數自身。
所以 Function.prototype.constructor === Function

Object
再說Object。(圖中色塊3)
我們平時見過這種創建函數的書寫形式:
const obj = new Object()
可見Object是一個函數。但同時函數又是一個對象。所以Object就是一個函數對象。
只要是函數對象,就會有原型prototype和隱式原型__proto__兩個屬性。
我們先看Object.prototype。(圖中色塊4)
Object作為一個函數,他就有自己的原型:Object.prototype。
根據口訣:所有函數的prototype都指向自身prototype:
所以 Object.prototype === Object.prototype(圖中線條d)

而對於Object.__proto__ 我們可以這樣理解:
Object作為一個函數,他是Function構造出來的。形似下面這種寫法:(圖中線條c)
const Object = new Function()
因此可以說Object是實例化函數對象。
根據口訣:一切 實例化對象的__proto__都指向構造函數的prototype 、 函數是Function構造出來的
所以 Object.__proto__ === Function.prototype。(圖中線條f)

原型的原型
我們來分析下兩個內置函數的原型的原型:
先看Function.prototype.__proto__
Function.prototype作為Function的原型對象,他就是一個普通對象,但凡普通對象就都是Object構造出來的,
根據口訣:實例化對象的__proto__都指向構造函數的prototype、所有prototype的__proto__都指向Object.prototype(Object的除外)
所以所有prototype對象的__proto__都指向構造函數Object的原型。包括Function函數的原型的隱式原型,也指向Object的原型。
所以 Function.prototype.__proto__ === Object.prototype。(圖中線條g)

再看Object.prototype.__proto__
Object.prototype作為是一個普通對象,他的隱式原型__proto__也本應該指向構造函數的原型。
但由於prototype對象都是Object函數構造的,按照上邊的規則,Object.prototype.__proto__也本應該指向Object.prototype。但是這麼死循環的指沒完沒了了不是,還沒有意義。
所以這裡是原型鏈中第二個特殊點:讓Object.prototype的原型指向null,好結束這段輪迴。
也就是 Object.prototype.__proto__ === null。(圖中線條e)
口訣:所有prototype的__proto__都指向Object.prototype(Object的除外)

好在,Object.prototye的構造函數還是誠實的,知道自己的祖宗是誰,於是他的constructor屬性還是Ojbect。
Object.prototype.constructor === Object

自定義函數
我們都知道,平時我們用字面量的形式創建一個對象、數組、function,
其實都是new Object()、或 new Array()、new Function 這樣的形式創建的。(圖中線條h)
var obj = {}; // 類似寫法 var obj = new Object();
var arr = []; // 類似寫法 var arr = new Array();
function Person() {} // 類似寫法 var Person = new Function(){}
所以對象、數組、函數這些都是實例化對象。
對象、數組稍後再談,他們就是自定義對象。(圖中色塊7)
先說我們創建的函數 — — 自定義函數。(圖中色塊5)
「自定義函數」和Object性質一樣,都是函數對象。只不過自定義函數的名字是我們用戶自定義的,比如Person、Animal、clickHandle等。而Object、Array等是JS內部原生提供的。
但記住口訣:只要是函數對象,就會有原型prototype和隱式原型__proto__兩個屬性。
先說自定義函數.prototype(圖中色塊6)。
前邊說過,所有函數的prototype都指向自身prototype,原型上邊的constructor再指回函數自身。
所以 Person.prototype === Person.prototype(圖中線條i)

再說自定義函數.prototype.__proto__
自定義函數的原型作為普通對象,由Object構造出來,其原型__proto__肯定指向Object.prototype。原理同Function.prototype。
根據口訣:實例化對象的__proto__都指向構造函數的prototype、所有prototype的__proto__都指向Object.prototype(Object的除外)
所以 自定義函數.prototype.__proto__ === Object.prototype。(圖中線條J)

再說自定義函數.__proto__
根據口訣:實例化對象的__proto__都指向構造函數的prototype
因此,實例化對象(這裡的自定義函數)的__proto__就指向構造函數的原型。根據口訣:函數是Function構造出來的、所有函數對象的__proto__都指向Function.prototype(包括Function自身)。所有自定義函數的構造函數是Function,他的原型也就是右邊的Function.prototype。
自定義函數.__proto__ === Function.prototype。(圖中線條k)

自定義對象
說清楚了自定義函數,我們再來說自定義對象。(圖中色塊7)
比如obj、arr這樣的對象,他們和我們平時「new + 構造函數()」得到的實例化對象一樣:(圖中線條L)
const object = new Object()
const person = new Person()
以這個person為例,說一下圖中綠色塊7:自定義對象
既然叫「自定義對象」,那他肯定就只是一個對象。
對象就好說了,普通對象身上只有__proto__,而且普通對象(實例化對象)的__proto__指向構造函數的prototype。
根據口訣:實例化對象的__proto__指向構造函數的prototype
即自定義對象.__proto__ 指向 自定義對象的構造函數(即自定義函數)的prototype
所以 person.__proto__ === Person.prototype。(圖中線條m)
於是圖中(實例化)自定義對象.__proto__ 指向了上邊自定義函數原型。

至此,這張圖我們都過了一遍。

總結
-
Function.__proto__ === Function.prototype【特殊】 -
Function.prototype === Function.prototype -
Function.prototype.constructor === Function -
Function.prototype.__proto__ === Object.prototype -
Object.__proto__ === Function.prototype。 -
Object.prototype.__proto__ === null 【特殊】 -
Person.__proto__ === Function.prototype -
Person.prototype.__proto__ === Object.prototype -
person.__proto__ === Person.prototype
原型鏈
由於原型對象prototype本身是一個對象,因此,他也有隱式原型__proto__。隱式原型指向的規則不變,指向構造函數的原型;
這樣一來,原型 -> 隱式原型、隱式原型 -> 原型。
從某個對象出發,依次尋找隱式原型的指向,將形成一個鏈條,該鏈條叫做原型鏈。
在查找對象成員時,若對象本身沒有該成員,則會到原型鏈中查找。

在上圖和知識總結中我們看到:
自定義對象的__proto__指向自定義函數的原型。
而自定義函數的原型也是一個對象,他雖然在函數一生下來就有了,但是他作為對象,也是Object函數對象構建的。因此自定義函數原型身上的__proto__指向Object的原型對象。
而Object.prototype又指向null。
觀察發現這最左邊的一條居然形成了一個鏈式指向:自定義對象 -> 自定義函數的原型 -> Object原型 -> null。
當我們在最低部的自定義對象身上尋找一個屬性或方法找不到的時候,JS就會沿著這條原型鏈向上查找,若找到就返回,直到null還查不到就返回undefined。

同樣的,函數 -> Function原型 -> Object原型 -> null, 也形成了原型鏈。當我們在函數身上調用一個方法或屬性時,根據原型鏈的查找規則,會一直層層向上查找到null。
這也就是為什麼,call、apply、bind這些函數是定義在Function原型身上的,我們也能用Person.call、Person.apply這樣調用;hasOwnProperty、isPrototypeOf這些函數是定義在Object原型身上的,我們也能用Person.isPrototypeOf、obj.hasOwnProperty這樣使用了。
function Person() {
console.log('我是Person函數');
}
let obj = new Object()
let person = new Person()
console.log(person.hasOwnProperty('a'));
// 原型鏈查找:person -> person.proto(即Person.prototype) -> Person.prototype.proto (即Object.prototype) 找到hasOwnProperty函數,執行調用
console.log(Person.call());
// 原型鏈查找:Person -> Person.proto(即Function.prototype) 找到call函數,執行調用
console.log(obj.xxx)
// 原型鏈查找:obj -> obj.proto(即 Object.prototype) -> null 沒找到,返回undefined
知識點擴展
函數對象和普通對象
普通對象是通過 new 函數() 創建/構造的
函數對象是通過 new Function() 構造的

所有對象都是通過 new 函數() 的方式創建的
-
該函數叫做構造函數; -
創建的對象被稱作實例化對象 -
對象賦值給變數後,變數中保存的是地址,地址指向對象所在的記憶體。
函數也是一個對象,他是通過 new Function() 創建的
原型對象 prototype
原型prototype的本質:對象。
prototype又稱作原型對象。
原型對象也有一個自己的原型對象:__proto__
所有的函數都有原型屬性prototype
默認情況下,prototype是一個Object對象。也就是說由Object構造函數創建,其原型指向Object的prototype。
prototype中默認包含一個屬性:constructor,該屬性指向函數對象本身
prototype中默認包含一個屬性:__proto__,該屬性指向構造函數的原型(默認情況是Object.prototype)
隱式原型 __proto__
所有的對象都有隱式原型:__proto__屬性
隱式原型是一個對象,指向創建該對象的構造函數的原型 prototype
在查找對象成員時,若對象本身沒有該成員,則會到隱式原型中查找。
層層向上知道Object.prototype。若到null還找不到則返回undefined。
❝
__proto__ 並不是語言本身的特性,這是各大廠商具體實現時添加的私有屬性,雖然目前很多現代瀏覽器的 JS 引擎中都提供了這個私有屬性,但依舊不建議在生產中使用該屬性,避免對環境產生依賴。生產環境中,我們可以使用 Object.getPrototypeOf 方法來獲取實例對象的原型,然後再來為原型添加方法/屬性。
來自《es6》<//es6.ruanyifeng.com/#docs/class>❞
隱式原型和原型出現的根本原因:
js沒有記錄類型的元數據。因此,js只能通過對象的隱式原型找到創建他的函數的原型,從而確定其類型。
特殊的兩個情況
Function的隱式原型指向自己的原型
Object原型的隱式原型指向null
兩個固定情況
所有函數的隱式原型,都指向Function的原型(包括Function函數自身)
所有函數原型的隱式原型,都指向Object的原型。(不包括Object原型對象自身)
constructor
原型中的constructor指向函數本身:

思考
Function原型上都有什麼?
執行下列程式碼,創建一個普通該函數。
function a(){}
觀察window.a在控制台的列印結果,展開a.__proto__,得到Function.prototype的所有默認屬性:

圖中可以看到,a.prototype.__proto__,即Function的原型中:
-
有函數方法:call、apply、bind、toString、constructor、Symbol;(標誌性就是call、apply、bind這仨) -
有屬性:arguments、caller、以及這倆屬性的getter和setter; -
最後還有對象:__proto__指向他的構造函數原型(也就是Object.prototype)
Object原型上都有什麼?

有函數方法:hasOwnProperty、isPrototypeOf、propertyIsEnumerable、toLocaleString、toString、valueOf、以及constructor
特殊的還有:get __proto__、set __proto__,估計是為了返回null給攔截的。
標誌就是get __proto__、set __proto__這倆
其他探索問題
數組函數的原型上都有什麼?
自定義函數的原型上都有什麼?

練手面試題
最後來兩道面試題,歡迎評論區一起探討:



讓我們一起攜手同走前端路, 關注公眾號【前端印記】即可。