[介紹] Javascript 原型鏈應該了解的概念

MowLi 微風
微風飛翔
Published in
7 min readNov 17, 2020

前言

若要介紹原型鏈,我們可以先從繼承說起,Java、C# 等物件導向語言擁有類別繼承的概念,也就是 Class,它就像一張藍圖,可以在內部定義之後透過 new 關鍵字建立物件。

而 JavaScript 在 ES6 後也有 Class ,但這並不是真的 Class,因為 JavaScript 的繼承是基於原型的方式來達成,物件直接從其他物件繼承,以 __proto__ 來連接物件之間彼此的關係,稱之為原型鏈(prototype chain)。

此篇文章只會說明原型鏈基本應該理解的東西,使用原型所需注意的細節真的很多,篇幅有限。

優點

比起類別繼承,原型繼承的方式具有彈性、可擴充性,且懂得如何利用原型能讓 JavaScript 更有效率地執行,並避開一些雷點,理解比較厲害的大大寫的程式碼(有些 LeetCode 的實作題目也有用到原型的方式)。

實作及圖解原型鏈(prototype chain)

首先,我們先來寫一段程式碼,假設有一個 User,名字為 noah,今年 20 歲:

上面以函式建構式 new 出一個 noah 物件,並傳入了 name、age 參數,不過這並沒有什麼特別的,於是接著加入以下程式碼:

你會發現很神奇,noah 明明沒有 getInfo 方法卻可以呼叫!?因為這就是原型鏈的概念:

  • 當 JavaScript 發現該物件並沒有你所找尋的屬性或方法時,它就會往該物件的原型找,若原型沒有,再往原型的原型找,直到 null。
  • 物件的 __proto__ 會連結到原型物件的 prototype。
  • prototype 包含原型物件的構造函式(constructor)、你所設立的屬性或方法,以及連結的 __proto__。

因此,JavaScript 發現 noah 物件沒有 getInfo( ),開始往 noah 的原型找,發現 User 裡面有 getInfo( ) 並回傳給你使用,如下圖。

若要觀察物件之間的關係可以用 __proto__ 或 Object.getPrototypeOf( ) 測試,以下結果證明 noah 確實連結到 User.prototype:

這樣你能了解平常在處理陣列時為什麼可以使用 .forEach()、.reverse(),而字串則能夠 .concat()、.splice() !因為當 JavaScript 找不到屬性或方法就會往原型找,而這些方法都已經被寫好在原型了。

那將方法設置在 Prototype 跟直接在 User 設置有什麼不同!啊不是都可以用?

一、在函式建構式設置

每個被建立的實體物件都會有相同的方法,浪費了記憶體空間。

二、在原型物件的 Prototype 上設置

每個被建立的實體物件都能共享該方法。

因此,理論上應盡量把實體物件常使用到的屬性或方法放在原型物件的 Prototype 上,藉此節省記憶體空間。

Object.create

剛剛都是以「函式建構式加上 new 關鍵字」來建立物件及連結原型物件,而 Object.create() 為第二種方法。

Object.create(proto [, propertiesObject])

該方法為「替一個新物件指定它所參考的原型物件」。但這種說法我認為並不好理解,因為其 __proto__ 的指向與函式建構式的不同:

  • Object.create( ) 建立的物件,其 __proto__ 是指向參考的物件
  • 函式建構式 new 建立的物件,其 __proto__ 是指向函式的 prototype

啊~先不要搞得那麼複雜,只要記住「Object.create( ) 所丟進去的物件參數等同於第一種方法的 .prototype 的屬性及方法 」就好,看程式碼會比較好理解:

[補充]:看完之後,你是否能理解 MDN 上這個複雜例子了呢~?此篇還沒有探討到 Object.create( ) 的繼承,未來如果有機會的話會再探討。

Object.create(null)

另外,Object.create(null) 可以建立一個完全沒有參考原型的空物件,這麼做的主要原因通常是我們需要一個非常乾淨的物件環境,避免有時候命名相撞導致原型被污染。

在上述程式碼中,即使將函式建構式指定為空物件,還是無法創造一個乾淨的物件;但是 dict2 真的就是空物件,這表示你甚至不能在 dict2 使用 Object 最基本的 toString() 與 ValueOf()。

所以,dict2 放的東西不用怕跟任何 Object 相撞,而且要客製化該物件的彈性程度比平常高出很多。

其它使用原型鏈事項

一、Prototype 應該儲存什麼

前面有提到將東西放在原型物件上能有效減少記憶體空間,並具有差不多快的效能(Maybe),但不是所有東西都要往原型物件塞。

在 Effective JavaScript 書中提及通常放到原型物件大多都是「方法」,若「屬性」要放到原型物件,可以先確認以下事項,否則,建議儲存到實體物件上。

屬性是真的需要被共用的資料;或者這個屬性的值基本上不會變動(類似 const)。

二、陣列避免使用 for-in 遍歷

for in 是以 key-value 結構的方式進行遍歷,但是這會遍歷到陣列其原型物件 prototype 的東西。

三、不要隨意修改 __proto__

  1. 這與效能有關,JavaScript 引擎有針對一些物件特性的操作做效能上的優化,隨意更動結構可能會破壞設定。
  2. 隨意改變物件的原型可能導致你無法預測接下來會出現什麼錯誤。
  3. 若要自訂物件之間的連結建議使用 Object.create(),避免用 __proto__。

補充:那麼 ES6 Class 是什麼?

  • 其他物件導向語言的 Class 並不是物件,是一種藍圖,直到真的 new 之前不會得到有物件產生。
  • ES6 的 Class 就是一個物件,然後你以該物件作為新物件連接的原型。
  • 為一種語法糖衣(syntactic sugar),因為從上述說明可得知原理都不變,皆屬於原型繼承的概念。
  • 以 class 宣告的物件沒有 new 關鍵字直接回報錯誤,避免忘記 new 而造成隱藏的 bug。

總結

這次對基本的原型鏈介紹也讓我更明白裡面的運作,其實有時看一看以為自己懂了,但有時一個瞬間又被搞混了,建議可以畫出原型鏈的概念圖,會讓自己對於細節清晰許多。

而本篇所運用的例子其實還沒介紹到對於 Prototype 與 Prototype 之間的繼承,以及一些操作原型鏈的 method、polyfill 概念…等,有時間的話會再補上一篇。

參考資料

  1. Herman, D. (2013). Effective JavaScript: 68 Specific Ways to Harness the Power of JavaScript.
  2. JavaScript: Understanding the Weird Parts.

以上為筆者自己對於原型鏈的理解及整理,若有錯誤或問題可以在留言區幫忙糾正,感謝 😃

--

--