The Most Familiar Stranger - JavaScript - 原型
Tags: javascript, beginners, chinese
Published: 2023-05-11 13:16:26

[分享記錄檔 YouTube](https://www.youtube.com/watch?v=a4-Wi7K-Lp0&list=PLal-nRIQU5sunSCOSeZZEzUrXUXF22gep&index=9)

## 原型（ Prototype ）

每個 JavaScript 物件都有一個原型物件，它作為該物件繼承屬性和方法的基礎。物件的原型可以通過 `__proto__` 屬性取用 ( ES6 開始使用 Object.getPrototypeof()、Object.setPrototypeof() 這兩個 accessors 取用 )，原型物件也是一個物件，有自己的原型。當一個物件呼叫一個方法或取用一個屬性時，它會先查找自身是否有該屬性或方法，如果沒有，則會透過原型鏈查找。透過原型，物件可以繼承來自其原型的屬性和方法。

## 原型鏈（Prototype Chain）

原型鏈是由原型物件組成的一個鏈，用於查找物件的屬性和方法。當一個物件取用一個屬性或方法時，它會先查找自身是否有該屬性或方法，如果沒有，則會查找其原型物件是否有該屬性或方法，如果還沒找到，則會繼續查找其原型的原型，直到找到為止。

以下是一個簡單的例子，說明嘗試存取屬性時會發生的事：

```jsx
// 利用含有 a 與 b 屬性的 f 函式，建立一個 o 物件：
let f = function () {
   this.a = 1;
   this.b = 2;
}
let o = new f(); // {a: 1, b: 2}

// 接著針對 f 函式的原型添加屬性
f.prototype.b = 3;
f.prototype.c = 4;

// 不要寫 f.prototype = {b:3,c:4}; 因為它會破壞原型鏈
// o.[[Prototype]] 有 b 與 c 的屬性：{b: 3, c: 4}
// o.[[Prototype]].[[Prototype]] 是 Object.prototype
// 最後 o.[[Prototype]].[[Prototype]].[[Prototype]] 成了 null
// 這是原型鏈的結末，因為 null 按照定義並沒有 [[Prototype]]。
// 因此，整個原型鏈看起來就像：
// {a: 1, b: 2} ---> {b: 3, c: 4} ---> Object.prototype ---> null

console.log(o.a); // 1
// o 有屬性「a」嗎？有，該數值為 1。

console.log(o.b); // 2
// o 有屬性「b」嗎？有，該數值為 2。
// o 還有個原型屬性「b」，但這裡沒有被訪問到。
// 這稱作「property shadowing」。

console.log(o.c); // 4
// o 有屬性「c」嗎？沒有，那就找 o 的原型看看。
// o 在「o.[[Prototype]]」有屬性「c」嗎？有，該數值為 4。

console.log(o.d); // undefined
// o 有屬性「d」嗎？沒有，那就找 o 的原型看看。
// o 在「o.[[Prototype]]」有屬性「d」嗎？沒有，那就找 o.[[Prototype]] 的原型看看。
// o.[[Prototype]].[[Prototype]] 是 Object.prototype，預設並沒有屬性「d」，那再找他的原型看看。
// o 在「o.[[Prototype]].[[Prototype]].[[Prototype]]」是 null，停止搜尋。
// 找不到任何屬性，回傳 undefined。
```

## 函式建構子（ Constructor ）

函式建構子是一種用來創建物件的函式，它使用 `new` 關鍵字創建一個新物件，並將該物件的原型設置為函式的 `prototype` 屬性。函式建構子也可以添加屬性和方法，透過原型，這些屬性和方法可以被所有由該函式建構的物件所共享。

因此當我們這樣寫時：

```jsx
let o = new Foo();
```

JavaScript 其實會做：

```jsx
let o = new Object();
o.[[Prototype]] = Foo.prototype;
Foo.call(o);
```

## 物件屬性特徵（Object Property Descriptor）

JavaScript 中的每個屬性都有一個物件，稱為屬性描述符（Property Descriptor），它描述了該屬性的特徵。物件屬性特徵描述了一個屬性的值 (value)、可枚舉性 (enumerable)、可寫性 (writable)、可配置性 (configurable) 等屬性。

- **`value`**：該屬性的值。預設值為 **`undefined`**。
- **`writable`**：如果為 **`true`**，則該屬性的值可以被修改。如果為 **`false`**，則該屬性的值無法被修改。預設值為 **`true`**。
- **`enumerable`**：如果為 **`true`**，則該屬性可以被 **`for...in`** 迴圈枚舉。如果為 **`false`**，則該屬性無法被枚舉。預設值為 **`true`**。
- **`configurable`**：如果為 **`true`**，則該屬性的特徵可以被修改，並且該屬性可以被刪除。如果為 **`false`**，則該屬性的特徵無法被修改，且該屬性無法被刪除。預設值為 **`true`**。

以下是一個修改屬性特徵的範例：

```jsx
let obj = {name: 'John'};

let descriptor = Object.getOwnPropertyDescriptor(obj, 'name');

console.log(descriptor); 
// {value: "John", writable: true, enumerable: true, configurable: true}

// 修改屬性特徵
descriptor.writable = false;
descriptor.configurable = false;

Object.defineProperty(obj, 'name', descriptor);

// 現在 name 屬性已變為不可寫和不可配置
obj.name = 'Bob';
delete obj.name;

console.log(obj); // {name: "John"}，不可修改和刪除
```

## Class

Class 是 ES6 中引入的一個新特性，它提供了一種更簡潔的方式來定義物件的屬性和方法。Class 本質上是一種特殊的函式建構子，它使用 `class` 關鍵字定義，並使用 `constructor` 方法定義屬性和方法。與傳統的函式建構子不同，class 定義的方法預設是不可枚舉的。

以下是一個範例說明 class 定義的方法預設是不可枚舉的：

```jsx
class MyClass {
  constructor(a, b) {
    this.a = a;
    this.b = b;
  }
  sum() {
    return this.a + this.b; 
  }
}

const myObj = new MyClass(2, 3);

for (let key in myObj) {
  console.log(key); // Output: a, b
}

console.log(Object.keys(myObj)); // Output: [ 'a', 'b' ]
```