---
# System prepended metadata

title: 為什麼 JavaScript 會有那麼多內建語法可以使用? - 原型鏈
tags: [JavaScript]

---

---
tags:  JavaScript
title: 為什麼 JavaScript 會有那麼多內建語法可以使用? - 原型鏈
---
# 為什麼 JavaScript 會有那麼多內建語法可以使用? - 原型鏈
當我們撰寫 JavaScript 的程式碼時，為什麼會有那麼多的內建方法可以用? 例如一組陣列可以使用 `.length` 來得知陣列有多少位址資料，陣列內建的方式又是從何而來?

這裡紀錄一些關於原型的筆記 : 
- 原型物件
    - 原型屬性
    - 原型繼承
- 原型鏈
- new Object( ) 自訂原型
- Object.create( ) 將其他物件作為原型
- 多層繼承實作
- 物件屬性特徵



## 原型物件
JavaScript 是一個物件導向的語言，許多內建的方法都是透過繼承原型屬性來的，當我們打開以 JS 撰寫的瀏覽器時，就建好基礎的原型方法。像是以下例子，原型的頂層為一個物件，其名稱為 **Object**。物件 `{}`、函式 `function`、陣列 `[]` ，都是繼承了這個原型物件內的屬性，再根據型別有自己的原生方法，例如陣列與函式的原生原型就不一樣。
```javascript 
console.log(Object) 
// 此為物件頂層，會發現一個名為 Object的函式
// 函式也是物件的一種，可以發現它擁有自己的物件屬性

console.log(Object())
// 執行 Object() 這個函式會 return 一個名為 Object的物件

console.log(Array.__proto__ === Function.__proto__) 
// true，__proto__ 都是繼承了物件

console.log(Array.prototype === Function.prototype) 
// false，原生原型不同
```
### 原型屬性
原型物件內建了很多原生屬性，我們可以自行增加共用的屬性，方式有兩個 : 
1. `__proto__` ，繼承原型，建立在物件上
    - 每個物件都有此屬性
    - 屬性特徵為可被列舉 
    - **原型鏈的關鍵**
2. `prototype` ，原生原型，建立在函式上
    - 函式才有此原生屬性
    - 屬性特徵為不可被列舉

為什麼以下比對是 true?
```javascript 
console.log( Object.__proto__.__proto__ === Object.prototype ) 
// true
```
因為物件原型 `Object` 本身是一個函式，而
- `Object` 這個函式繼承了 `function` 的原生原型
- `function` 又繼承了原型物件 `Object` 的原生原型

所以它們指得是同樣的東西，可以使用 `console.dir(Object)` 驗證。(可以先往下看原型繼承，再回來想一下。)

---


### 原型繼承
以下比對可以了解繼承原型與原生原型。

首先，物件、函式與陣列本身為 ``"function"`` 。
```javascript 
console.log(typeof Object)   // "function"
console.log(typeof Array)    // "function"
console.log(typeof Function) // "function"
```

#### Object 原生原型與繼承原型
```javascript 
/*  Object */
console.log( Object.prototype === Object.__proto__ ) 
// false，前者是物件原生原型之物件，後者是繼承函式原型之函式
// Object，本身是為名為 Object的函式

console.log( Object.__proto__.__proto__ === Object.prototype ) 
// true，前者是繼承函式原型又繼承物件原型之物件，後者是原生原型之物件
```

#### Function 原生原型與繼承原型
```javascript 
/*  Function  */
console.log( Function.prototype === Object.__proto__ )  
// true，前者是函式原生原型之函式，後者是繼承函式原型之函式

console.log( Function.prototype === Function.__proto__ )  
// true，前者為函式原生原型之函式，後者為繼承函式原型之函式

console.log( Function.__proto__ === Object.__proto__ )
// true 

console.log( 
  Function.__proto__.__proto__ === 
  Object.__proto__.__proto__ 
)
// true 
```

#### Array 原生原型與繼承原型
```javascript 
/* Array */
console.log( Array.prototype === Object.__proto__ ) 
// false，前者為陣列原生原型之物件，後者為繼承函式原型之函式
// typeof Array.prototype 為 "object"

console.log( Array.prototype === Array.__proto__) 
// false，前者為陣列原生原型之物件，後者為繼承函式原型之函式
```

#### Function and Array 原生原型與繼承原型
```javascript
/* Function and Array*/
console.log( Function.prototype === Array.prototype )
// false，原生原型不同

console.log( Function.__proto__ === Array.__proto__ )
// true，均為繼承函式原型之函式

console.log( 
  Function.__proto__.__proto__ === 
  Array.__proto__.__proto__ 
)
// true
```
在往後撰寫 JS程式碼時，就可以知道正在使用的陣列、函式以及物件是繼承誰的屬性。

---

## 原型鏈
- 擁有物件的特性
- 會往上層搜尋
- 原型屬性可共用，不須額外撰寫
    - 可以新增屬性在原型中並賦予值，可以是陣列、函式、物件、純值等等...
    - 其他物件可以利用點記號取用

例如下面的範例 :  
```javascript 
var arrayA = [1, 2, 3]
var arrayB = [4, 5, 6]
arrayA.name = 'X陣列'
arrayB.name = 'Y陣列'
arrayA.getLast = function(){
  return this.name
}
console.log(typeof arrayA)
console.log(arrayA.getLast())
console.log(arrayB.getLast())
// "object"
// "X陣列"
// arrayB.getLast is not a function 
```
上面的範例中 : 
- `arrayA` ，可以使用 getLast這個方法
- `arrayB` ，卻無法使用 getLast這個方法，因為只建立在 `arrayA` 這個物件下
- 若要讓 `arrayB` ，可以使用 getLast這個方法，就得再建立一次

那如何讓方法共用呢? 我們可以利用**原型鏈**，新增一個共用方法在陣列這個實體物件上，像是這樣 : 
```javascript 
var arrayA = [1, 2, 3]
var arrayB = [4, 5, 6]
arrayA.name = 'X陣列'
arrayB.name = 'Y陣列'
// 使用 __proto__ ，其繼承了陣列的原生原型
// 在這個原型屬性上建立一個新方法
arrayA.__proto__.getLast = function(){
  return this.name
}
console.log(arrayA.getLast())
console.log(arrayB.getLast())
// "X陣列"
// "Y陣列"
```
不過上述的例子無法使用 `prototype` ，因為這個實體陣列型別為 "object"，它沒有這個原型屬性，只有函式才有，而 `__proto__` 直接在繼承來的物件上新增共用方法。除非新增在原生原型 `Array.prototype.getLast()`，這樣所有陣列都能共用。

---

## new Object( ) 自訂原型
只需要在原型自訂方法，就不用浪費一堆記憶體空間(不用一直重開)。

舉個例子來說，打開某個編輯應用程式時，會問你是否套入範本，然後再問你基本資料要填入甚麼。
以下範例是一個構造函式，把它想成是一範本，利用這個範本去建立一個實體檔案，這個範本函式裏頭可以先寫上未來要成為物件時，所擁有的屬性，以建構式運算子建立時，就會成為該物件的屬性。而範例中 : 
- 每個物件實體繼承了構造函式的屬性名稱，同時也繼承了物件的原生屬性
- 每個物件實體有自己的屬性值
- 不同物件時體會新開記憶體位置
- 每個物件實體之物件屬性為自己的原生原型
```javascript 
function motorCycle(name, color, displacement){
  this.name = name
  this.color = color
  this.displacement = displacement
  this.congratulate = function(){
    console.log('Congratulations on the deal! Have a nice trip!')
  }
}

var motoA = new motorCycle('Z900RS', 'red', 948)
var motoB = new motorCycle('R1000', 'blue', 1000)
console.log(motoA.name)
console.log(motoB.color)
motoB.congratulate()
console.log(motoA.congratulate === motoB.congratulate)
// "Z900RS"
// "blue"
// "Congratulations on the deal! Have a nice trip!"
// false
```
雖然上述範例中，每個物件實體都能使用相同的方法，但它們屬於不同的記憶體位址，直接寫在構造函式中也不易閱讀，接著讓我們來改寫上述的範例 : 
- 將構造函式的方法移動到原生原型上，也就是物件原生原型
- 結果變成了 true，代表它們參考同樣的位址

```javascript 
function motorCycle(name, color, displacement){
  this.name = name
  this.color = color
  this.displacement = displacement
}
motorCycle.prototype.congratulate = function(){
  console.log('Congratulations on the deal! Have a nice trip!')
}
var motoA = new motorCycle('Z900RS', 'red', 948)
var motoB = new motorCycle('R1000', 'blue', 1000)

console.log(motoA.congratulate === motoB.congratulate)
console.log(motoA.__proto__ === motorCycle.prototype ) 
console.log(motorCycle.__proto__ === Function.prototype ) 
// true
// true，motoA繼承了 motorCycle的原生原型，但沒有繼承函式的原生原型
// true，motorCycle繼承了 Function的原生原型
```
### 建構式物件繼承了甚麼
不知道看到這裡會不會有個疑問，`motoA` 這個物件實體怎麼沒有繼承函式的原生原型，讓我們用 `console.log(motoA.__proto__)` 檢查它繼承了甚麼?
- `congratulate`
    - 繼承 motorCycle的原生原型之自訂方法
- `constructor`
    - 繼承 motorCycle這個函式
    - 可以利用它來**辨認是從哪一個建構函式繼承的**
- `__proto__`
    - 繼承物件的原生原型
    - 建構式物件型別為 "object"


### 如何得知該方法為物件所有還是繼承的
可以利用 `hasOwnProperty` 來檢查。
```javascript 
var array = [1, 2]
array.forEach((item)=>{
  console.log(item)
})
console.log( array.hasOwnProperty('forEach')) 
console.log( array.hasOwnProperty('length')) 
console.log( array.__proto__.hasOwnProperty('forEach')) 
// 1
// 2
// false
// true
// true
```

---

## Object.create( ) 將其他物件作為原型
除了使用構造函式轉為建構物件來自訂原生原型屬性，也可以將另一個物件直接作為建構物件使用。
1. 自訂構造函式 > 建構物件 > 實體物件
2. 將另一個物件屬性作為建構物件 > 實體物件
    - 新的實體物件透過 `Object.create(另一物件)` 將其作為原生原型屬性(包含屬性值)
    

```javascript=
function motorCycle(name, color, displacement){
  this.name = name
  this.color = color
  this.displacement = displacement
}
motorCycle.prototype.congratulate = function(){
  console.log('Congratulations on the deal! Have a nice trip!')
}

var motoA = new motorCycle('Z900RS', 'red', 948)
var motoB = new motorCycle('R1000', 'blue', 1000)

var motoC = Object.create(motoA)
motoC.color = 'black'
console.log(motoC)

console.log(motoC.prototype === motoA.prototype) 
// true
// 原生原型看起來相同，但要注意，建構物件型別為 "object"
// 也就是說，它們沒有 prototype這個屬性，是 undefined

console.log(motoC.__proto__ === motoA) 
// true
// 繼承另一個物件屬性作為原型
```
但是上面的範例這樣做會有點問題，繼承來的原生原型是一個物件，若不變更屬性值，基本上它們的 `console.log()` 內容一樣，但它是利用原型鏈向上層找，本身沒有自己的屬性，像這樣 : 
```javascript=
/* 這是 motoC */
console.log(motoC)
// 在開發者模式下的結果
// motorCycle {
//   __proto__ : motorCycle
// }

/* 這是 motoA */
console.log(motoA)
// motorCycle {
//   name: 'Z900RS'
//   color: 'red'
//   displacement: 948
//   __proto__ : Object
// }
```
不過，還是可以利用 `Object.create()` 來快速複製屬性，例如繼承多個物件屬性時會用到它。


## 多層繼承實作
JS無法同時繼承多個物件，但是可以使用多層繼承的方式，實作中我們會用到 : 
- `Object.creat()`
- `new Object()`
- `prototype`

```javascript 
// Object > Transportation > 汽車、摩托車、飛機 > 保時捷911、Z900RS、A320

/* 交通工具 */
function Transportation(tool){
  this.toolName = tool || '交通工具'
}
// 在頂層交通工具函式原型上新增共用方法
Transportation.prototype.move = function(){
  console.log(this.toolName + '向前移動')
}
Transportation.prototype.congratulate = function(){
  console.log('Congratulations on the deal! Have a nice trip!')
}

/* 交通工具類別*/
function motorCycle(name, color, displacement){
  // 執行 Transportation.call() 並填入工具名稱，完成該工具類別屬性
  // 未加入任何原型
  Transportation.call(this, '摩托車')
  this.name = name
  this.color = color
  this.displacement = displacement
}
// 將交通工具類別的原型更改為頂層交通工具
motorCycle.prototype = Object.create(Transportation.prototype)
// motorCycle.prototype = Transportation.prototype
// 這樣也是可以運作，但是這樣變成傳參考，修改 motorCycle.prototype時，
// 也會一併修改 Transportation.prototype


// 將交通工具的類別的建構函式改回自己，否則為頂層交通工具的建構函式
// 有助於辨別從哪個函式轉來
motorCycle.prototype.constructor = motorCycle

// 將交通工具的類別，新增一個只有該類別才能用的函式
motorCycle.prototype.wheelie = function (){
  console.log('翹孤輪~')
}
// 雖然它的繼承原型還是 Transportation，
// 但是同一層的建構函式已修改回原本交通工具類別，還是可以辨認

var motoA = new motorCycle('Z900RS', 'red', 948)
motoA.move() // "摩托車向前移動"
motoA.wheelie() // "翹孤輪~"
```

## 物件屬性特徵
當我們使用原型來新增方法時，有沒有發現到有個東西會用到，但實際上看不到它?

### prototype 
當我們新增一個方法在函式原型上，為什麼使用 console.log()時，卻找不到 `prototype` 呢? 讓我們使用 `Object.getOwnPropertyDescriptor` 這個方式來檢查該 prototype 這個物件屬性，
```javascript 
// 創造一個構造函式
function proto(name){
  this.name = name
}
// 接著，在原型上新增一個方法
proto.prototype.fnA = function (){
  console.log('函式')
}
console.log(Object.getOwnPropertyDescriptor(proto, 'prototype'))

// Object {
//   value: {
//     fnA: funciton(){
//       console.log('函式')
//     },
//     constructor: ƒ proto(name),
//     __proto__: Object{...}
//   }
//   writable: true, // 可寫入
//   configurable: false, // 不可被刪除
//   enumerable: false, // 不可被列舉
//   __proto__: Object{...}
// }
```
我們可以發現，`prototype` 這個屬性設定的其中兩個為 false
- configurable 可否被刪除
- enumerable 可否被列舉

很多教學建議新增原型方法時，盡量設定在這個屬性上，但實際上又看不到它，而上述就是為什麼看不到 `prototype` 這個屬性的原因。


<!-- ## new Object() 和 Object.create()
- 建立屬性的位置不同
- 屬性的設定不同

### 透過 new Object()
- 建立在自身屬性上
- 屬性設定均為 true

### 透過 Object.create()
- 建立在 `__proto__` 這個原型屬性上
- 屬性設定均為 false
 -->

## 參考來源
>1. 六角學院 - JS核心篇
>2. [Huli- 該來理解 JavaScript 的原型鍊了](https://blog.techbridge.cc/2017/04/22/javascript-prototype/)
>3. [MDN - 繼承與原型鏈](https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Inheritance_and_the_prototype_chain)
>4. [深入剖析Object.create()，為與之相關的理解形成閉環](https://www.mdeditor.tw/pl/p77l/zh-tw)
