【克服 JS 】第五章 JavaScript 的物件導向與原型繼承

JavaScript 全攻略:克服 JS 的奇怪部分,第五章 物件導向與原型繼承(Prototypal Inheritance),學習紀錄。

Udemy 課程連結:

JavaScript 全攻略:克服 JS 的奇怪部分

5-1 觀念小叮嚀:古典和原型繼承(classical vs prototypal inheritance)

  • 繼承(Inheritance):一個物件取用另一個物件的屬性或方法。
  • 古典繼承(Classical Inheritance):C#、JAVA常用到的物件繼承方式,古典繼承很流行,也解決了很多問題,但樹狀結構物件的互動模式,一但數量增加,很容易產生複雜、龐大的集合。
  • 原型繼承(Prototypal Inheritance):JavaScript 的物件繼承種類,相對古典繼承簡易、彈性,既然是所謂原型 (Prototypal),就代表有相對於目前物件的原型存在。

5-2 瞭解原型(Prototypal)

在 JavaScript 中所有的物件,包括函式,都會有個 proto 屬性,這個屬性指向了一個物件 proto。

假如有一個物件 obj,它有個屬性 prop1,而 obj 的原型物件 proto,有個屬性 prop2。

當要存取 obj 物件的屬性會這樣寫:

obj.prop1
obj.prop2
  1. 當使用 obj.prop1 時會取用 obj 的屬性 prop1 沒問題。
  2. 而使用 obj.prop2 時,因為 obj 沒有 prop2 屬性,會到 obj 的原型物件 proto 去找,有的話就取用 proto 的屬性 prop2。

同樣的,物件 proto 也有自己的原型物件,若找不到某個屬性時就會再往上層的原型物件找,這個尋找的路徑就叫原型鏈(Prototype Chain)。

例如在 proto 原型物件的 proto 原型物件,新增屬性 prop3,當呼叫 prop3 屬性時,會一層一層去找。

原型鏈查找與範圍鏈(外部參照)是不一樣的東西,前者是去物件的原型對象找屬性和方法,後者是往外部執行環境找變數,這兩者不可搞混。

使用原型範例:

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

var john = {
  firstname: 'John',
  lastname: 'Doe'
}

// 以下為了解說方便複寫了原型物件__proto__,實際上不會那麼做,會增加瀏覽器負擔
john.__proto__ = person
console.log(john.getFullName()) // John Doe
console.log(john.firstname) // John

var jane = {
  firstname: 'Jane'
}

jane.__proto__ = person
console.log(jane.getFullName()) // Jane Default

雖然 john 物件的成員函數沒有 getFullName,但他的原型物件有,所以一樣可以使用 john.getFullName(),而且此時 getFullName 中的 this 是指向呼叫的物件 john,不會指向原型物件 person。

而另一個物件 jane 因為缺少了 lastname,在使用 getFullName 時,因為找不到 jane.lastname,會到原型物件 person 去找,所以顯示為 'Default'。

5-3 所有的東西都是物件(或純值)

JavaScript 中所有的東西都是物件,所以都有各自的原型 只有一種物件沒有原型,就是基本物件(base object)。

直接在 Chrome 瀏覽器 console 輸入:

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

物件的原型就是一個基本物件,各種物件的原型鏈最後都會指向一個基本物件。

輸入 a.__proto__,可以看到是一個物件,並且有很多屬性,若我們輸入 a.__proto__.,就會跳出 a 物件原型可存取的屬性、方法。

輸入 a.__proto__.__proto__,可以看到原型鏈終點,也就是 null。

5-3 Reflection 與 Extend

Reflection:一個物件可以看到自己的東西,然後改變自己的屬性和方法。

利用 Reflection 可以實現一個有用的模式,叫做 Extend 用來將一個物件的所有成員複製到另一個物件裡。

沿用 5-1 節的範例:

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

var john = {
  firstname: 'John',
  lastname: 'Doe'
}

john.__proto__ = person

展示一個 reflection 的例子:

for(var prop in john) {
  console.log(prop + ': ' + john[prop])
}

// firstname: John
// lastname: Doe
// getFullName: function () {
//   return this.firstname + ' ' + this.lastname
// }

使用 Forin 語法,可以將目標內的東西全都遍歷、loop個一輪,將每次迴圈時,成員的名稱存成變數 prop,使用 john[prop] 就可以取得該成員的值。

john 其實沒有 getFullName 這個方法,因為 john 原型 person 有,Forin 還是順著原型鏈把其他屬性都取出來了。

使用基本物件提供的 hasOwnProperty(),可以檢查這個成員是不是自己的,將 Forin 程式碼改成:

for(var prop in john) {
  if(john.hasOwnProperty(prop)) {
    console.log(prop + ': ' + john[prop])
  }
}

// firstname: John
// lastname: Doe

hasOwnProperty() 可以檢查屬性是不是該物件本身的成員,若有非物件本身的屬性存在(包含物件原型的屬性),就回傳false,因此這裡的 if 判斷,console.log 只會印出 john 本身有的屬性。

以 underscore.js 的 extend 函式作為範例:

var john = {
  firstname: 'John',
  lastname: 'Doe'
}

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

var jim = {
  getFirstName: function() {
  return firstname;
  }
}

_.extend(john, jane, jim)
console.log(john.address) // 111 Main St.
console.log(john.getFormalFullName()) // Doe, John
console.log(john.getFirstName()) // John

使用 extend 後,物件 john 獲得了另外兩個物件的屬性與方法。有 extend 可以用,就不需要每次都使用原型鏈查找。現在可以直接操作 john 最初沒有的屬性了。

新版本的 ES6 有新增 extends,在 ES6 之前的各種框架也有出現 extends,辦並不衝突。ES6 的 extends,在現在使用 Vue、React 開發很常看到。

參考資料:

JavaScript 基礎二三事|2018 iT 邦幫忙鐵人賽

那克斯的學習筆記

TOP