# JavaScript的原型繼承(Prototypal Inheritance)
###### tags: `JavaScript` `Interview Preparation`
:::warning
:bulb: 本站筆記已同步更新到我的[個人網站](https://simplydevs.netlify.app/)囉! 歡迎參觀與閱讀,體驗不同的視覺感受!
:::
[TOC]
## :memo: 前言
在[MDN文件](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Inheritance_and_the_prototype_chain)中提到,JavaScript並非一個以class為基礎(class-based)的語言(例如Java、C++),儘管在JavaScript中有`class`這個關鍵字,但那只是為了開發者撰寫更直觀易懂的語法糖;事實上,**JavaScript是以原型為基礎(prototype-based)的語言**。
## :memo: 繼承(Inheritance)
### 什麼是繼承
繼承(Inheritance)可以說是物件導向程式設計(object-oriented programming, OOP)最重要的原則之一,繼承可以讓子類別(child class/subclass)沿用父類別(parent class/ superclass)的屬性與功能,以[MDN文件](https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Objects/Object-oriented_programming)的例子而言:
```
class Professor
properties
name
teaches
constructor
Professor(name, teaches)
methods
grade(paper)
introduceSelf()
```
以上例子中,定義了一個`Professor`類別,而這個類別中有兩個屬性(properties): `name` 和 `teaches`,以及兩種方法(methods):`grade()` 和`introduceSelf()`
類別就像是一個模板,可以創造該類型的物件,每個被創造出來的物件稱為該類別的**實例(Instance)**,而創造實例的過程則是透過一種特殊的函式- **建構式(Constructor)** 達成。
至於什麼是繼承呢,再來看另一個類別`Student`:
```
class Student
properties
name
year
constructor
Student(name, year)
methods
introduceSelf()
```
在`Student`這個類別中,可以發現和`Professor`類別擁有相同的屬性`name`以及相同的方法 `introduceSelf()`,因此我們可以定義一個`Person`類別作為這兩個類別的父類別,讓這兩個類別繼承`Person`的屬性或方法:
```
class Person
properties
name
constructor
Person(name)
methods
introduceSelf()
class Professor : extends Person
properties
teaches
constructor
Professor(name, teaches)
methods
grade(paper)
introduceSelf()
class Student : extends Person
properties
year
constructor
Student(name, year)
methods
introduceSelf()
```
### 繼承的優點
- **提高程式碼的重複使用性**
假如我們有一個class A,並且想再建立一個包含class A部分程式碼的class B,可以透過繼承從class A衍生class B,重複使用class A的資料與方法。
- **避免程式碼重複**
繼承可以在多個子類別中共享程式碼,減少程式碼重複;如果兩個相關的類別擁有類似的程式碼,我們可以將這些程式碼放進父類別中。
- **提高程式碼靈活性及延展性**
如果需要更改,可以在父類別中更改並由子類別繼承,換言之,父類別的屬性和方法所做的更改都可以直接應用在子類別上,所有公共的屬性和方法都可以直接在父類別宣告。另一方面,子類別也可以加入新的屬性或方法。
- **提供更佳的程式碼結構與管理**
繼承使得子類別必須遵照標準的介面(interface)進行延伸,提供了方便理解的程式碼結構
- **保留父類別的完整性**
宣告子類別並不影響父類別的原始碼,因此可以保留父類別的完整性。這也是封裝性(Encapsulation)的特性展現。
- **隱藏數據**
父類別可以將某些數據設為私有,子類別無法更改或取用
- **幫助達成執行環境的多型(Polymorphism)**
透過繼承,子類別可以加入不同的執行方式(implementation),覆寫(override)父類別的方法
*Ref: [MDN doc](https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Objects/Object-oriented_programming), [Inheritance in OOPS: An Idea of Code Reusability](https://www.enjoyalgorithms.com/blog/inheritance-in-java)*
### JavaScript的原型繼承
以下這兩段來自MDN文件的描述,快速說明了JS原型繼承的特性:
> **某些人認為 JavaScript 並非真正的物件導向 (Object-oriented, OO) 語言。** 在「典型 OO」中,你必須定義特定的類別物件,才能定義哪些類別所要繼承的類別。**JavaScript 則使用不同的系統 —「繼承」的物件並不會一併複製功能過來,而是透過原型鍊連接其所繼承的功能,亦即所謂的原型繼承 (Prototypal inheritance)。**
From [MDN文件](https://developer.mozilla.org/zh-TW/docs/Learn/JavaScript/Objects/Classes_in_JavaScript)
:::warning
關於物件導向程式設計,可以參考[這篇文章](https://www.educative.io/blog/object-oriented-programming)
:::
#### 原型鏈的頂端是物件
> JavaScript 就只有一個建構子:物件。每個物件都有一個連著其他原型(prototype)的私有屬性(private property)物件。**原型物件也有著自己的原型,於是原型物件就這樣鏈結,直到撞見 null 為止:null 在定義裡沒有原型、也是原型鏈(prototype chain)的最後一個鏈結。 幾乎所有 JavaScript 的物件,都是在原型鏈最頂端的物件實例。**
> From [MDN文件](https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Inheritance_and_the_prototype_chain)
實際看看以下的程式碼:
```javascript=
const milk = 1
const milkProto = Object.getPrototypeOf(milk)
const milkProtoProto = Object.getPrototypeOf(milkProto)
const milkProtoProtoProto = Object.getPrototypeOf(milkProtoProto)
console.log(milkProto)
// {}
console.log(milkProtoProto)
// [Object: null prototype] {}
console.log(milkProtoProtoProto)
// null
```
`console.log(milkProto)`這一行程式碼在console中可以看到:

它的原型中包含建構式(constructor)`Number()`函式以及各種這個原型建構的實例可以使用的方法(method),`[[Prototype]]` 則可以觀察到這個`Number`的原型是`Object`,也就是原型鏈的上一層。
`console.log(milkProtoProto)`這一行程式碼在console中則可以看到:

`__proto__: (...)` 也就是再往原型鏈的上層找不到東西了,所以`console.log(milkProtoProtoProto)`印出的是`null`
## :memo: 範例
說了那麼多,直接用程式碼操作:
> Note: 以下例子使用[`class`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes)語法糖撰寫,和傳統的建構函式會略有不同,關於兩者的比較也可以參考[另一篇文章](https://hackmd.io/@yy933/B1MGs4Tni)。
### `extends`和`super`
```javascript=
class Drink {
constructor(name, cost) {
this._name = name
this._cost = cost
}
// getter
get name() {
return this._name
}
get cost() {
return this._cost
}
get amount(){
return this._amount
}
// method
amountAdded(){
this._amount++
}
}
```
先定義一個class`Drink`,接著定義一個class`Coffee`:
(關於屬性名稱為何要使用底線,請參考[這個問題](https://stackoverflow.com/questions/54562790/cannot-set-property-which-only-has-getter-javascript-es6))
```javascript=
class Coffee {
constructor(name, cost, origin) {
this._name = name
this._cost = cost
this._origin = origin
}
get name() {
return this._name
}
get cost() {
return this._cost
}
get origin() {
return this._origin
}
}
```
class`Coffee`的屬性和方法,繼承自class`Drink`,可以使用`extends`關鍵字進行繼承,所以改寫`Coffee`如下:
```javascript=
class Coffee extends Drink {
constructor(name, cost, origin) {
super(name, cost);
this._origin = origin;
}
get origin(){
return this._origin
}
get coffeeInfo() {
return `The ${this._name} from ${this._origin} costs ${this._cost} dollars.`
}
}
```
:pushpin: Points:
- `extends`關鍵字讓`Coffee`可以使用父類別`Drink`中的**方法**。
- `super`關鍵字**呼叫父類別中的建構式,並可以取用父類別的屬性與方法**。在以上範例中,`super(name, cost)`將`name`和`cost`兩個argument傳入父類別`Drink`中的建構式,並執行產生新的`Coffee`物件實例。
- 值得注意的是,**當使用建構式時,`super`關鍵字必須在`this`關鍵字之前使用,確保新的物件已經在父類別的建構式中建立,此時`this`會指向這個新建立的物件**;如果沒有在`this`之前呼叫`super`,則會出現`reference error`,好的做法是**在子類別建構式的第一行使用`super`關鍵字**。
- `origin`是`Coffee`中的新屬性,所以在此處的建構式中定義它。
::: success
`class`語法糖中,我們可以直接把共用的方法(method)寫在class裡面;如果是用ES6以前的建構式,寫法相當於:
```javascript=
// amountAdded function
Drink.prototype.amountAdded = function () {
return this._amount++
}
```
或者是使用[`Object.assign`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign)的語法:
```javascript=
const amountAdded = {
function () {
return this._amount++
}
}
Object.assign(Drink.prototype, amountAdded);
```
另外,在繼承父類別時,`class`語法糖使用`extends`關鍵字;ES6以前則可以使用[`call()`](https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Reference/Global_Objects/Function/call) 函式,getter、setter則可以使用[`Object.defineProperty()`函式](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty)。
Reference: [MDN docs - Object prototypes](https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Objects/Object_prototypes)
:::
接著,用子類別`Coffee`創造一個新的實例看看:
```javascript=
const latte = new Coffee('latte', 5, 'Brazil')
console.log(latte.name)
// output: latte
```
上述程式碼中,我們創造了一個新的`Coffee`實例`latte`,因為`latte`可以取用父類別`Drink`中的getter`name`,所以會回傳儲存在`this._name`屬性中的值,也就是latte。
接著再執行以下程式碼:
```javascript=
latte.amountAdded()
console.log(latte.amount)
// output: 1
```
以上程式碼做了什麼呢?
1. `Coffee`繼承了父類別`Drink`的`_amount`、amount getter、以及`amountAdded`函式
2. 當我們建立了實例`latte`,`Drink`的建構式把`_amount`屬性設為0
3. 因為繼承了`amountAdded`函式,所以新建立的實例`latte`可以呼叫這個方法並執行,使儲存在`_amount`屬性的值+1
4. 最後呼叫amount getter,所以會回傳儲存在`this._amount`屬性中的值,也就是1
如果我們再定義另一個子類別`Tea`,同樣繼承`Drink`的屬性和方法:
```javascript=
class Tea extends Drink {
constructor(name, origin) {
super(name);
this._origin = origin;
}
get origin(){
return this._origin
}
}
```
一樣試試看是否可以調用name getter:
```javascript=
const blackTea =new Tea('Black Tea', 'India')
console.log(blackTea.name)
// output: Black Tea
```
成功調用name getter! 再來看看和`Tea`的原型和剛剛建立的`Coffee`是否相同:
```javascript=
console.log(Object.getPrototypeOf(Tea) === Object.getPrototypeOf(Coffee))
// output: true
```
### 靜態方法(Static Methods)
有時,我們希望class中具有個別實例中不可調用的方法,但可以直接從該class中調用這些方法。這些方法稱為靜態方法(Static Methods)。這些方法可以用`static`關鍵字調用。
延續前面的範例:
```javascript=
class Drink {
constructor(name, cost) {
this._name = name
this._cost = cost
this._amount = 0
}
get name() {
return this._name
}
get cost() {
return this._cost
}
get amount(){
return this._amount
}
amountAdded(){
this._amount++
}
static rating(){
const randomNumber = Math.floor(Math.random()*5)
return randomNumber
}
}
```
直接從`Drink`調用`rating`方法:
```javascript=
console.log(Drink.rating())
// output: <random rating>
```
繼承類別,也會繼承靜態方法:
```javascript=
console.log(Coffee.rating())
// output: <random rating>
```
但是如果是該類別或繼承的子類別所建立的實例,調用該方法:
```javascript=
const drink = new Drink('drink', 2)
console.log(drink.rating())
// TypeError: drink.rating is not a function
const latte = new Coffee('latte', 5, 'Brazil')
console.log(latte.rating())
// TypeError: latte.rating is not a function
```
則會出現錯誤,因為無法從實例上調用靜態方法。
最後再看一個例子:
`Math`是一個JavaScript的內建物件,它擁有多種靜態屬性與方法。比較特別的是,和大多數的全域物件(global object)不同,`Math`不是建構式(constructor),也就是說,無法使用`new`運算子來建立一個實例(instance),但是可以直接從`Math`調用靜態方法,例如調用[`Math.log()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/log)這個靜態方法:
```javascript=
console.log(Math.log(1))
// output: 0
```
---
## 參考資料
* MDN文件
* [原型繼承與原型鏈](https://javascript.alphacamp.co/prototype-prototype-chain.html)
* [Day13-圖解原型繼承與原型鏈](https://ithelp.ithome.com.tw/articles/10289866)
* [Prototypal inheritance](https://javascript.info/prototype-inheritance)
* [Day 02: JavaScript 與 物件導向程式設計](https://ithelp.ithome.com.tw/articles/10265849#:~:text=%E5%B0%81%E8%A3%9D%E6%80%A7%EF%BC%88Encapsulation%EF%BC%89&text=%E4%B8%AD%E6%96%87Wiki%20%E5%AE%9A%E7%BE%A9%EF%BC%88%E9%80%A3%E7%B5%90%EF%BC%89%EF%BC%9A,%E8%83%BD%E4%B8%8D%E8%83%BD%E8%A2%AB%E9%9A%B1%E8%97%8F%E8%B5%B7%E4%BE%86%E3%80%82)
* [Inheritance in OOPS: An Idea of Code Reusability](https://www.enjoyalgorithms.com/blog/inheritance-in-java)
* [[教學] JavaScript ES6 Class:深入淺出類別概念與應用](https://www.shubo.io/javascript-class/#%E7%B9%BC%E6%89%BF%E9%9D%9C%E6%85%8B%E6%96%B9%E6%B3%95-static-method)
* Codecademy 教材
::: success
:crescent_moon: 本站內容僅為個人學習記錄,如有錯誤歡迎留言告知、交流討論!
:::