Understand JavaScript #20 物件導向與原型繼承
本文主要內容為探討「原型」的相關知識,包含原型繼承、原型鏈、基本物件,以及資源庫 Underscore 裡面的 Reflection 與 Extend 模式。
古典繼承 vs. 原型繼承
- 繼承 (Inheritance):一個物件可以取用另一個物件的屬性或方法
- 古典繼承 (Classical Inheritance):出現最久也最受歡迎,應用在 C# 和 Java 等語言,缺點是一旦數量龐大就會很複雜,而且有很多關鍵字要記憶與學習
- 原型繼承 (Prototypal Inheritance):簡單易懂,具有彈性與可擴充性,JavaScript 用原型繼承來分享物件的屬性和方法
原型鏈
- 原型 (Prototype):所有的物件(包含函式)都有一個
proto
屬性,這個屬性會參考到另一個物件,而被參考到的物件就是原型- 如果在主要物件上找不到想要取用的屬性,就會往原型去找,所以雖然屬性看起來是在主要物件上,但其實是在一個稱為原型鏈的東西上
- 原型鏈 (Prototype Chain):透過原型屬性
proto
連結著,讓主要物件在這上面取用屬性和方法
我們直接透過程式碼來理解原型的概念吧。
現今的瀏覽器有提供方法可以直接取用原型,但是非常不建議實際使用,因為運行效能很差,只能在 Demo 說明時使用。
1var person = { 2 firstname: 'Default', 3 lastname: 'Default', 4 getFullName: function () { 5 return this.firstname + ' ' + this.lastname; 6 }, 7}; 8 9var damao = { 10 firstname: 'Damao', 11 lastname: 'Huang', 12}; 13 14// Don't do this EVER! for example purposes only. 15damao.__proto__ = person; // (1) 16console.log(damao.getFullName()); // (2) Damao Huang 17console.log(damao.firstname); // (3) Damao 18
- 將
damao
的原型屬性設定為person
,意思就是damao
繼承自person
。換句話說,就是damao
原來的本質被設定為person
。 - 在
damao
裡面找不到getFullName
方法時,會往原型proto
尋找。注意:此時方法中的this
會指向呼叫函式的物件damao
。 - 使用
damao.firstname
在damao
物件找到firstname
之後就會結束了,不會再進入原型鏈。
基本物件
在 JavaScript 中,所有的東西都是物件或純值,而且它們都有原型,然而只有一個東西沒有原型,那就是「基本物件」。
基本物件就是原型鏈最末端(後代)的東西,如果再往上找(祖先)則會得到 null
。
1var a = {}; 2var b = function () {}; 3var c = []; 4var d = ''; 5 6console.log(a.__proto__); // Object {} → 基本物件 7console.log(a.__proto__.__proto__); // null 8 9console.log(b.__proto__); // ƒ () { [native code] } 10console.log(b.__proto__.__proto__); // Object {} 11console.log(b.__proto__.__proto__.__proto__); // null 12 13console.log(c.__proto__); // [] 14console.log(c.__proto__.__proto__); // Object {} 15console.log(c.__proto__.__proto__.__proto__); // null 16 17console.log(d.__proto__); // "" 18console.log(d.__proto__.__proto__); // Object {} 19console.log(d.__proto__.__proto__.__proto__); // null 20
Underscore 的 Reflection 與 Extend 模式
- Extend:另一個建立物件的函式,它不是 JavaScript 的原生方法,而是資源庫 Underscore 裡面出現的方法,在其他資源庫中也有類似的方法
- Reflection:簡單來說就是讓 JavaScript 的物件可以看見與改變自己的屬性與方法
- 藉由 Reflection,我們才能做到 Extend
我們來看看 Underscore 資源庫的 extend 方法是如何運作的。
Reflection
首先 Reflection 的運行原理就類似使用基本物件的 hasOwnProperty
方法,會去檢查該物件是否有某個屬性或方法,而且跟 for...in
不一樣,這個方法並未檢查物件的原型鏈。
為了解釋觀念,以下程式碼只是在概念上差不多而已,與 Underscore 的原始碼當然是不一樣的。
1var person = { 2 firstname: 'Default', 3 lastname: 'Default', 4 getFullName: function () { 5 return this.firstname + ' ' + this.lastname; 6 }, 7}; 8 9var damao = { 10 firstname: 'Damao', 11 lastname: 'Huang', 12}; 13 14damao.__proto__ = person; // Don't do this EVER! for example purposes only. 15 16// 1. 遍歷物件裡的每個東西 17for (var prop in damao) { 18 console.log(prop + ': ' + damao[prop]); // 使用中括號,因為 prop 是字串 19} 20 21// 2. 只取得自己本身的東西 22for (var prop in damao) { 23 // 後代可以使用基本物件的方法 24 if (damao.hasOwnProperty(prop)) { 25 console.log(prop + ': ' + damao[prop]); 26 } 27} 28
- 使用
for...in
遍歷物件裡的每個東西,除了物件damao
本身的屬性和方法,for...in
也會取得原型上的屬性和方法。 - 如果只想取得自己本身的東西,可以使用基本物件的
hasOwnProperty
方法,這個就類似於 Reflect 的動作。
知道了 Reflection 的概念後,我們來看看怎麼使用 Extend 這個模式。
Extend
使用 Extend 時,第一個參數是想要延長的物件(一個後代),而後方第二、第三個參數的物件可以放很多個(多個祖先),所以最後會有一大串東西加到我們的 damao
物件裡面。
1var sean = { 2 address: '111 Main St.', 3 getFormalFullName: function () { 4 return this.lastname + ' ' + this.firstname; 5 }, 6}; 7 8var sealman = { 9 getFirstName: function () { 10 return this.firstname; 11 }, 12}; 13 14_.extend(damao, sean, sealman); 15console.log(damao); 16
由此可以看出,Underscore 的 _.extend()
跟原型鏈的概念不同,它是把很多屬性結合放到一個物件上。
在實作時,我們不一定只能用原型鏈,使用 Underscore 提供的 Reflection 與 Extend 模式也很好用,而我自己也是比較喜歡 Underscore 的寫法與邏輯。
回顧
看完這篇文章,我們到底有什麼收穫呢?藉由本文可以理解到…
- JavaScript 原型繼承與原型鏈的概念
- 基本物件是原型鏈的最末端
- 資源庫 Underscroe 裡面的 Reflection 與 Extend 模式