徹底搞清楚 JavaScript 的原型和原型鏈

金色海洋(jyk)發表於2021-12-02

JavaScript真的挺無語的,怪不得看了那麼多的介紹文章還是一頭霧水,直到自己終於弄懂了一點點之後才深有體會:
先從整體說起吧,發現沒有基礎做依據,那都是空中樓閣;
先從基礎開始介紹吧,又發現基礎是個蛇頭咬蛇尾的圓環,無從下手,應該先整體介紹。
於是介紹本身就成了一個死迴圈。。。

還是嘗試著從基礎開始。。。(多圖預警)

主要內容:

  • 物件的繼承樹。
  • 函式的繼承樹。
  • 函式 VS 物件
  • prototype VS _ _ proto__
  • 繼承 VS 組合
  • 自己定義函式(class),以及實現繼承

尋找原型鏈的“源頭”

網上有一個梗:萬物基於MIUI。雖然是一句調侃,但是也表達源頭的重要性。

看過一些高手寫的關係圖,應該是非常專業,但也正是因為太專業了,所以才導致新手看的是一頭霧水。
那麼對於先手來說,有沒有簡單一點的方式呢?我們可以借鑑一下物件導向的思路。

提到物件導向,大家都會想到基類(超類、父類)、子類、繼承、多型等。為啥容易記住呢?因為繼承關係非常簡單,從基類開始,一層一層繼承下去,結構非常清晰明瞭。

我覺得應該借鑑一下這種表達方式,也許這種方式並不契合JavaScript,但是我覺得應該比較方便初學者入門。

經常聽說,JavaScript 的世界是基於 Object 的,這句話對但是又不對,為啥這麼說呢?我們來看看 Object 的結構:(使用 console.dir() 可以看到細節 )

console.dir(Object)

Object的結構組成

首先請注意一下那個 f 的標識,這表示 Object 其實是一個函式(從 JavaScript 的語法角度來看),我們來驗證一下:

Object 其實是函式

這到底是怎麼回事呢?後面細說,先把找到源頭才好理解。

這個 Object 並不是源頭,因為還有 prototype 和 __ proto__, 我們先看看 Object.prototype 的結構:

Object.prototype

console.dir(Object.prototype)

Object原型的結構

可以看到,Object.prototype 才是源頭,因為 Object.prototype 沒有 prototype(當然沒有),_ _ proto__ 也是 null,我們來驗證一下:

console.dir(Object.prototype.prototype)
console.dir(Object.prototype.__proto__)

Object 驗證prototype

Object 驗證 proto

Object.__proto __

103-object2.png

這是啥?是不是很頭暈,這個其實指向的是 Function的原型,我們來驗證一下:

103-object2驗證.png

這是咋回事?後面再解釋。

小結

是不是有點暈,讓我們來梳理一下思路:

Object的三個重要屬性

如果看上面的圖有點暈的話,可以先看下面的圖,灰線說的是建構函式的關係,可以先跳過。(終於畫出來了那種繞圈圈的圖,向著專業又邁出了一步)

Object的兩個重要屬性

  • 思路一:
    Object有兩個屬性,一個是物件原型,一個是函式原型。

  • 思路二:
    Object有兩個指標,一個指向物件原型,一個指向函式原型。

我覺得思路二更適合一些,這個是理解 JavaScript 的原型鏈的第一個門檻,如果繞不清楚的話……沒關係,往下看就好,我也是把下面都寫出來,然後回頭才整理出來這個圖的。。。(這個也是給繼承和組合做個鋪墊)

構建一顆大樹 —— 物件的繼承關係

找到源頭之後,我們就可以構建一顆大樹了。

構建原則:xxx.prototype._ _ proto__ === Object.prototype
即:Object.prototype 看做父類,然後把其“子類”畫出來。

物件的樹

這下是不是清晰多了呢?我們來驗證一下:

  • Array

190-Array原型.png

  • String:

String原型

好長好長,差點截不下來。

  • Number

Number原型

  • BigInt

BigInt原型

  • Boolean

Boolean原型

  • Symbol

Symbol原型

  • Date

Date原型

  • RegExp (正規表示式)

RegExp原型

  • Math

Math

共同點

每種型別都有自己的成員,然後_ _ proto__ 指向 Object.prototype。

特例

這裡有幾個特殊情況:

  • Math
    沒有原型,或者說原型就是 Math 自己。

  • Array
    這個比較奇怪。

  • null 和 undefined
    這對兄弟先當做特殊情況來處理。

  • Function
    Function.prototype._ _ proto__ 也是指向 Object.prototype的,但是 Function.prototype 是一個 f

  • Object
    如果說 Object.prototype 是基類的話,那麼Object是啥呢?其實 Object 是函式。
    是不是有點暈?從JavaScript 語法的角度來說,不僅 Object 是函式,String、Number這些都是函式。

再構建一顆大樹 —— 函式的繼承關係

觀察上面的圖(物件的樹)可以發現,我寫的都是xxx.prototype 的形式,那為啥不直接寫xxx呢?

因為從 JavaScript 的語法的角度來看,Object、String、Number、Array、Function等都是函式,Object.prototype、String.prototype 等才是物件。
我們從函式的角度來構造另一顆大樹。

依據:xxx._ _ proto__ === Function.prototype
即:把Function.prototype看做父類,把他的子類(__ proto__指向他的)都畫出來。

函式的樹

這裡加上“()”,明確一下,然後我們來看一下具體的結構:

  • Function

200-Function原型.png

  • String

String()

  • Number

Number()

  • Boolean

Boolean()

  • BigInt

BigInt()

  • Symbol

Symbol()

  • Date

Date()

  • RegExp

RegExp()

  • Array

Array()

物件 VS 函式

物件和函式的樹都畫完了,然後我們來分析一下物件和函式的區別。

  • 物件:是一個容器,可以存放各種型別的例項(資料),包括函式。
  • 建構函式:依據原型建立原型的例項。(個人理解可能有誤)
  • 一般函式:就是我們“隨手”寫的函式,執行某些程式碼,返回結果(也可以不返回)。

從 JavaScript 的語法角度來看,Object、Function、String、Date、Number等都是function,而Object.prototype、String.prototype、Date.prototype、Number.prototype等才是物件。

這和我們經常用到表述方式有點不一樣,也正是這個原因,導致理解和表達的時候非常混亂。

我們還是來驗證一下:

  • 函式

函式的驗證.png

  • 物件

物件的驗證.png

這裡有一個特例,Function.prototype 是一個函式,而且是所有函式的源頭。

所以說,從 JavaScript 的語法角度來看,函式就是函式,物件就是物件,不存在Object既是物件又是函式的情況。

那麼到底是什麼關係呢?我們定義一套函式來具體分析一下。

實戰:用ES6的class定義一套物件/函式

ES6提供了class,但是這個並不是類,而是 Function 的語法糖。
目的是簡化ES5裡面,為了實現繼承而採用的各種“神操作”。

用class來定義,結構和關係會非常清晰,再也不會看著頭疼了,建議新手可以跳過ES5的實現方式,直接用ES6的方式。

我們先定義一個Base,然後定義一個Person繼承Base,再定義一個Man繼承Person。
也就是說,可以深層繼承。

class Base {
  constructor (title) {
    this.title = '測試一下基類:' + title
  }

  baseFun1(info) {
    console.log('\n這是base的函式一,引數:', info, '\nthis:', this)
  }
}

class Person extends Base{
  constructor (title, age) {
    super(title)
    this.title = '人類:' + title
    this.age = age
  }

  personFun1(info) {
    console.log('\n這是base的函式一,引數:', info, '\nthis:', this)
  }
}


class Man extends Person {
  constructor (title, age, date) {
    super(title, age)
    this.title = '男人:' + title
    this.birthday = date
  }

  manFun3 (msg) {
    console.log('jim 的 this ===', this, msg)
  }
}

我們列印來看看結構:

例項的結構.png

  • 建構函式 constructor
    列印結果很清晰的表達了,建構函式就是我們定義的class。

  • 屬性
    屬性比較簡單,統統都掛在 this 上面,而且是同一個級別。

  • 函式
    函式就有點複雜了,首先函式是分級別的,掛在每一級的原型上面。
    Base的函式(baseFun1),掛在Base的原型上面,_ _ proto__ 指向原型。
    Person的函式(PersonFun1),應該掛在Person的原型上面,但是列印結果似乎是,Base好像標錯了位置。

  • 原型鏈
    Man的例項 > Man的原型 > Person的原型 > Base 的原型 > Object 的原型。
    通過 _ _ proto__ 連線了起來。

Man的例項 man1,可以通過這個“鏈條”,找到 baseFun1,直接用即可(man1.baseFun1()✔),
而不需要使用_ _ proto__(man1.__ proto__.__ proto__.__ proto__.baseFun1()✘)

這個和麵對物件的繼承是一樣的效果。

prototype VS _ _ proto _ _

看上面兩個大樹,既有 prototype 又有 _ _ proto _ _,好亂的樣子。那麼怎麼辦呢?我們可以找一下規律:

  • prototype:
    prototype 是自己的原型,可以其原型可以是函式,也可以是物件。有各自的繼承體系。

  • __ proto __ :
    __ proto __ 指向上一級原型,並不是自己的,只是一個指標,方便使用父級的方法和屬性。
    可以指向物件,也可以指向函式。

原型和原型鏈

組合 VS 繼承

一提到物件導向,大家一般都會想到封裝、繼承、和多型。但是 JavaScript 卻不是這個思路。
上面那顆大樹看起來是繼承的關係,Object.prototype 是基類,派生出來 Object.prototype、Function.prototype、string.prototype等。

但是其實這裡面隱藏了組合的方式。

我們展開Object 來看看,就會發現自己進入了一個迷宮

object

object 的“特殊”的結構

看上面的圖我們會發現,Object 並不像我們想象的那麼簡單,有很多的方法,我們隨便找幾個點開看看:

object展開三個函式

每一個函式都有自己的原型鏈(__ proto__),是不是有一種進入迷宮的感覺?我當初看的到時候就被嚇退了,這都是個啥?

但是我們換個思路來理解,就清晰多了,那就是:組合代替繼承!

Object 其實是由若干個函式組合而成。

其實,想一想,JavaScript 沒有私有成員,所以各種細節都暴露出來了,所以我們可以看到原型,看到原型鏈,看到建構函式。

正是因為看到了這麼多的細節,而以前又沒有一個比較好的封裝方式,所以看起來就特別的亂,理解起來也特別頭疼。

總結

按照 JavaScript 的語法來總結,否則總感覺說不清楚。

  • 物件(xxx.prototype)

    • 物件的“根”是 Object.prototype,其上一級是null。
    • 物件只有_ _ proto__,指向上一級原型。
    • 物件沒有 prototype,因為Object.prototype、String.prototype、Number.prototype等本身就是物件。通過 _ _ proto__尋找上一級原型。
  • 函式

    • 函式的“根”是 Function.prototype,其上一級是 Object.prototype。
    • 函式有prototype,(JavaScript 語法角度)Object、String、Function、Number 等都是函式,同時也是其原型的建構函式。
    • 函式有_ _ proto__,指向上一級函式。
  • 例項只有_ _ proto__ ,指向函式原型。

相關文章