古典繼承 vs. 原型繼承

  • 繼承 (Inheritance):一個物件可以取用另一個物件的屬性或方法。
  • 古典繼承 (Classical Inheritance):出現最久也最受歡迎,應用在 C# 和 Java 等語言,缺點是一旦數量龐大就會很複雜,而且有很多關鍵字要記憶與學習。
  • 原型繼承 (Prototypal Inheritance):簡單易懂,具有彈性與可擴充性,JavaScript 用原型繼承來分享物件的屬性和方法。

什麼是原型

  • 原型 (Prototype):所有的物件(包含函式)都有一個 proto 屬性,這個屬性會參考到另一個物件,而被參考到的物件就是原型。

    • 如果在主要物件上找不到想要取用的屬性,就會往原型去找,所以雖然屬性看起來是在主要物件上,但其實是在一個稱為原型鏈的東西上。
  • 原型鏈 (Prototype Chain):透過原型屬性 proto 連結著,讓主要物件在這上面取用屬性和方法。

Prototype

我們直接透過程式碼來理解原型的概念吧。

現今的瀏覽器有提供方法可以直接取用原型,但是非常不建議實際使用,因為運行效能很差,只能在 Demo 說明時使用。

var person = {
  firstname: 'Default',
  lastname: 'Default',
  getFullName: function() {
    return this.firstname + ' ' + this.lastname;
  },
};

var sealman = {
  firstname: 'Sealman',
  lastname: 'Huang',
};

// Don't do this EVER! for example purposes only.
sealman.__proto__ = person; // (1)
console.log(sealman.getFullName()); // (2) Sealman Huang
console.log(sealman.firstname); // (3) Sealman
  1. sealman 的原型屬性設定為 person,意思就是 sealman 繼承自 person
    換句話說,就是 sealman 原來的本質被設定為 person
  2. sealman 裡面找不到 getFullName 方法時,會往原型 proto 尋找。
    注意:此時方法中的 this 會指向呼叫函式的物件 sealman
  3. 使用 sealman.firstnamesealman 物件找到 firstname 之後就會結束了,不會再進入原型鏈。

基本物件

在 JavaScript 中,所有的東西都是物件或純值,而且它們都有原型,然而只有一個東西沒有原型,那就是「基本物件」。

基本物件就是原型鏈最末端(後代)的東西,如果再往上找(祖先)則會得到 null

var a = {};
var b = function() {};
var c = [];
var d = '';

console.log(a.__proto__); // Object {} → 基本物件
console.log(a.__proto__.__proto__); // null

console.log(b.__proto__); // ƒ () { [native code] }
console.log(b.__proto__.__proto__); // Object {}
console.log(b.__proto__.__proto__.__proto__); // null

console.log(c.__proto__); // []
console.log(c.__proto__.__proto__); // Object {}
console.log(c.__proto__.__proto__.__proto__); // null

console.log(d.__proto__); // ""
console.log(d.__proto__.__proto__); // Object {}
console.log(d.__proto__.__proto__.__proto__); // null

Underscore 的 Reflection 與 Extend 模式

  • Extend:另一個建立物件的函式,它不是 JavaScript 的原生方法,而是資源庫 Underscore 裡面出現的方法,在其他資源庫中也有類似的方法喔。
  • Reflection:簡單來說就是讓 JavaScript 的物件可以看見與改變自己的屬性與方法。
  • 藉由 Reflection,我們才能做到 Extend。

我們來看看 Underscore 資源庫的 extend 方法是如何運作的。

Reflection

首先 Reflection 的運行原理就類似使用基本物件hasOwnProperty 方法,會去檢查該物件是否有某個屬性或方法,而且跟 for...in 不一樣,這個方法並未檢查物件的原型鏈。

這裡只是概念上差不多而已,與 Underscore 的原始碼當然是不一樣的。

var person = {
  firstname: 'Default',
  lastname: 'Default',
  getFullName: function() {
    return this.firstname + ' ' + this.lastname;
  },
};

var sealman = {
  firstname: 'Sealman',
  lastname: 'Huang',
};

sealman.__proto__ = person; // Don't do this EVER! for example purposes only.

// 1. 遍歷物件裡的每個東西
for (var prop in sealman) {
  console.log(prop + ': ' + sealman[prop]); // 使用中括號,因為 prop 是字串
}

// 2. 只取得自己本身的東西
for (var prop in sealman) {
  // 後代可以使用基本物件的方法
  if (sealman.hasOwnProperty(prop)) {
    console.log(prop + ': ' + sealman[prop]);
  }
}
  1. 使用 for...in 遍歷物件裡的每個東西,除了物件 sealman 本身的屬性和方法,for...in 也會取得原型上的屬性和方法。
  2. 如果只想取得自己本身的東西,可以使用基本物件hasOwnProperty 方法,而這邊其實就類似 Reflect 的動作了。

知道了 Reflection 的概念後,我們來看看怎麼使用 Extend 這個模式。

Extend

使用 Extend 時,第一個參數是想要延長的物件(一個後代),而後方第二、第三個參數的物件可以放很多個(多個祖先),所以最後會有一大串東西加到我們的 sealman 物件裡面。

var sunny = {
  address: '111 Main St.',
  getFormalFullName: function() {
    return this.lastname + ' ' + this.firstname;
  },
};

var shirogoma = {
  getFirstName: function() {
    return this.firstname;
  },
};

_.extend(sealman, sunny, shirogoma);
console.log(sealman);

由此可以看出,Underscore 的 _.extend() 跟原型鏈的概念不同,它是把很多屬性結合放到一個物件上。

在實作時,我們不一定只能用原型鏈,使用 Underscore 提供的 Reflection 與 Extend 模式也很好用,而我自己也是比較喜歡 Underscore 的寫法與邏輯。

回顧

看完這篇文章,我們到底有什麼收穫呢?藉由本文可以理解到…

  • JavaScript 原型繼承與原型鏈的概念。
  • 基本物件是原型鏈的最末端。
  • 資源庫 Underscroe 裡面的 Reflection 與 Extend 模式。

References