在某天,我聽了一個老師的公開課,一張圖搞懂了原型鏈。

老師花兩天時間理解、整理的,他講了兩個小時我們當時就聽懂了。


今天我把他整理出來,分享給大家。也讓我自己鞏固加深一下。

就是這張圖:

為了更好的圖文對照,我為每條線編了標號,接下來的細節講解,都會用到這張圖裡的編號:

為了你更好的對照閱讀,你可以單獨開啟這張圖片,然後對比著文章看。

當然,我後邊也會貼心的把對應區域截小圖貼在文案附近。

前置知識

在對這張圖進行詳細拆解前,我們先來說幾個前置的基礎知識。以便後續更好的理解。

  • 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__

口訣提煉

為了更好的掌握,我把相關的知識點彙總成下列幾條口訣。接下來的剖析中都會用到。

  1. 函式是Function構造出來的
  2. 一切函式都是物件
  3. 只要是函式物件,就會有原型prototype和隱式原型__proto__兩個屬性。
  4. 普通物件身上只有__proto__,沒有prototype
  5. 例項化物件的__proto__都指向建構函式的prototype
  6. 所有函式的prototype都指向自身prototype
  7. 所有prototype的__proto__都指向Object.prototype(Object的除外)
  8. 所有函式物件的__proto__都指向Function.prototype(包括Function自身)
  9. 物件身上都有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》<http://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__這倆

其他探索問題

陣列函式的原型上都有什麼?

自定義函式的原型上都有什麼?

練手面試題

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

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