前端入門15-JavaScript進階之原型鏈

請叫我大蘇發表於2018-12-05

宣告

本系列文章內容全部梳理自以下幾個來源:

作為一個前端小白,入門跟著這幾個來源學習,感謝作者的分享,在其基礎上,通過自己的理解,梳理出的知識點,或許有遺漏,或許有些理解是錯誤的,如有發現,歡迎指點下。

PS:梳理的內容以《JavaScript權威指南》這本書中的內容為主,因此接下去跟 JavaScript 語法相關的系列文章基本只介紹 ES5 標準規範的內容、ES6 等這系列梳理完再單獨來講講。

正文-原型鏈

原型鏈也就是物件的繼承結構,舉個例子:

var a = []

那麼 a 物件的原型鏈:

a -> Array.prototype -> Object.prototype -> null

基本所有物件的原型鏈頂部都是 Object.prototype,而 Object.prototype 沒有原型,手動通過 Object.create(null) 建立的物件也沒有原型。但這兩點是特例。

原型的用途在於讓物件可繼承原型上的屬性,達到功能複用、程式碼複用的目的。

物件導向的程式語言中,繼承是一大特性,所以在編寫 JavaScript 程式碼時,要能夠很明確所建立的物件的一個原型鏈結構,這樣才便於更好的設計,更好的編寫程式碼。

在編寫程式碼過程中,使用的無非就是內建物件,或者自定義物件,所以下面來看看兩者的原型鏈結構:

內建物件的原型鏈結構

其實也就是之前有講過的預設的原型鏈結構:

  • 宣告的每個函式 -> Function.prototype –> Object.prototype -> null
  • 陣列物件 -> Array.prototype -> Object.prototype -> null
  • 物件直接量建立的物件 -> Object.prototype -> null
  • 日期物件 -> Date.prototype -> Object.prototype -> null
  • 正則物件 -> RegExp.prototype -> Object.prototype -> null

    可以用物件的 __proto__.constructor.name 來測試:

前端入門15-JavaScript進階之原型鏈

Object.prototype 已經內建定義了一些屬性,如:toString(),isPrototypeOf(),hasOwnProperty() 等等;

同樣,Array.prototype 內建瞭如:forEach(),map() 等等。

其他內建原型也都有相對應的一些屬性。

所以使用內建物件時,才可以直接使用內建提供的一些屬性。

自定義物件的原型鏈結構

不手動修改自定義建構函式的 prototype 屬性的話,預設建立的物件的原型鏈結構:

  • 自定義建構函式建立的物件 -> {} -> Object.prototype -> null

比如:

function A() {}
var a = new A();

在首次使用建構函式 A 時,內部會去對 prototype 屬性賦值,所進行的工作類似於:A.prototype = new Object();

所以 A.prototype 會指向一個空物件,但這個空物件繼承了 Object.prototype。

那麼不修改這條原型鏈的話,預設通過自定義建構函式建立的物件的繼承結構也就是:{} –> Object.prototype –> null。

雖然這條原型鏈也可以這麼表示:A.prototype –> Object.prototype -> null

a 雖然確實繼承自 A.prototype,但我不傾向於這種寫法來表示,因為自定義建構函式的 prototype 屬性值會有很大的可能性被修改掉,當它的屬性值重新指向另一個物件後,此時也仍舊可以說 a 物件繼承自 A.prototype,個人感覺理解上會有點彆扭,無法區別前後原型的不同,畢竟 A.prototype 只是一個 key 值,所以我傾向於直接說 a 繼承的實際物件,也就是 key 值對應的 value 值。

雖然 Object.prototype 也是一個 key 值,實際指向的一個內建的物件,但手動修改這些內建建構函式的 prototype 的可能性不高,所以個人覺得對於內建建構函式,可以直接用類似 Object.prototype 來表示。

那麼這個時候,如果為這個建構函式的 prototype 新增一些屬性:

function A() {}
A.prototype.num = 0;
var a = new A();

那麼,對於物件 a 而言,它的原型鏈:

a -> {num:0} -> Object.prototype -> null

這是不修改原型鏈的場景,那麼如果手動破壞了預設的原型鏈呢?

var B = [];
B.num = 0; 
function A() {}   
A.prototype.num = 222; 
var a = new A();  //a 的原型鏈
A.prototype = B;
var b = new A();  //b 的原型鏈

此時物件 b 的原型鏈又是什麼呢?

首先看看物件 B,是一個陣列物件,所以 B 物件的原型鏈:

B –> Array.prototype -> Object.prototype -> null

再來看看物件 a,建立它時,還並沒有修改建構函式的 prototype,所以它的原型鏈:

a -> {num:222} -> Object.prototype -> null

那麼這個時候,手動修改掉了建構函式的 prototype 指向,這之後再通過建構函式 A 建立的物件的原型鏈也就會跟隨著變化,所以物件 b 的原型鏈:

b -> B –> Array.prototype -> Object.prototype -> null

所以,修改建構函式的 prototype,其實相當於將另外一條原型鏈拿來替換掉原本的原型鏈。

原型鏈用途

對於物件,它的本質其實也就是一堆屬性的集合,所以物件的用途是用來操作物件內的屬性的,而當操作物件的屬性時,會有一種類似於作用域鏈機制來尋找屬性。

操作無非分兩種場景,一是讀取物件屬性,二是寫物件屬性,兩種所涉及的處理不一樣。

當讀取物件屬性時,是依靠物件的原型鏈來輔助工作,如果物件內部含有該屬性,則直接讀取,否則沿著原型鏈去尋找這個屬性。

也就是說,物件繼承原型的機制,並不是說,將原型的所有屬性拷貝一份到物件內部,而只是簡單對物件建立一條原型鏈而已。這條原型鏈中儲存著各個原型物件的引用,當讀取繼承的屬性時,就可以根據這條原型鏈上的引用訪問到其他原型物件內的屬性了。

因為讀取繼承屬性,本質上是讀取其他物件的屬性,那麼,這些原型屬性發生變化時,也才會影響到繼承他們的子物件。

那麼,對於寫物件屬性的操作:

這點就由物件的特性決定了:當對一個物件的屬性進行賦值操作時,如果物件內沒有該屬性,那麼會動態為該物件新增一個屬性,如果物件內部有該屬性,那麼修改屬性值。

物件的屬性寫操作會影響到後續的讀操作,因為如果是讀取物件的某個繼承屬性,本來物件內部沒有該屬性,所以是去讀取的原型內的屬性值。但經過寫操作後,物件內部建立了同名的內部屬性,之後再讀取時,發現內部已經有了,自然不會再去原型鏈中讀取。

獲取物件的原型鏈

掌握了原型鏈的相關理論,對於程式碼中某個物件的原型鏈也就能夠很清楚的知道了。無外乎內建物件的預設原型鏈,或者自定義建構函式手動修改的原型鏈。

但,初學階段,如果想借助瀏覽器的開發者工具的 console 來測試、檢視物件的原型鏈以便驗證猜想,可以這麼處理:

var a = []

前端入門15-JavaScript進階之原型鏈

雖然 __proto__ 可以獲取原型,但拿到的是物件,所以可以藉助物件的某些標識,比如原型的 constructor 的 name 函式名屬性標識。

例項

前端入門15-JavaScript進階之原型鏈

網上關於原型鏈的文章經常會出現這麼一張圖片,首先我承認,這張圖很高階,也基本把原型鏈的相關理論表示出來了,但我很不喜歡它。因為對於新手來說,很難看懂這張圖,我第一次看到也一臉懵逼。

就算現在能夠看懂了,我也還是不喜歡它,因為這張圖表達的內容太多了:它不僅表示了某個物件的原型鏈結構,同時,也表示出了例項物件、原型、建構函式三者間的函式,而建構函式本質上也是物件,所以也順便表示它的原型鏈結構。

我們一步步來看,它首先定義了一個建構函式 Foo,然後通過它建立了 f1,f2物件,然後從 f1,f2開始出發,先求他們的原型鏈。

用程式碼來說,其實也就是:

function Foo() {}
var f1 = new Foo();
//求f1物件的原型鏈

根據我們上述梳理的理論,很簡單了吧,原型鏈其實也就是:

f1 -> {} -> Object.prototype -> null

接著,它表達了可以用 __proto__ 獲取物件的原型,然後每個原型、建構函式、例項物件三者間的關係它也表達出來了,原型的constructor指向建構函式,而建構函式的prototype指向原型。

而這三個角色本質上也都是物件,既然是物件,那麼它們本身也有原型,所以也再順便畫出它們的原型鏈。

總之,就是從 f1 例項物件出發,先找它的原型,通過原型再找建構函式,然後再分別將原型和建構函式看成例項物件,重複之前f1的工作。

另外,又通過 new Object() 建立了物件 o1,求它的原型鏈。

所以,這張圖上,其實表達了一共 5 條原型鏈,分別是:

  • f1 的原型鏈
  • f1 的原型的constructor指向的建構函式Foo物件的原型鏈
  • 函式物件Foo的原型的constructor指向的建構函式Function物件的原型鏈
  • f1 的原型的原型即Object.prototype的constructor指向的建構函式Object 物件的原型鏈。
  • o1 的原型鏈

如果你能從這張圖看出這5條原型鏈,那麼原型鏈的理論你就基本掌握了。

而且,建議看這張圖時,每次都將某條原型鏈跟蹤到底,再去看另一條,這過程不要過多關注在分支上,否則很容易混亂。

對於新手,如果能夠對這張稍作備註,而不是直接將這張圖放出來,我覺得會更好,如下:

前端入門15-JavaScript進階之原型鏈


大家好,我是 dasu,歡迎關注我的公眾號(dasuAndroidTv),公眾號中有我的聯絡方式,歡迎有事沒事來嘮嗑一下,如果你覺得本篇內容有幫助到你,可以轉載但記得要關注,要標明原文哦,謝謝支援~
dasuAndroidTv2.png

相關文章