iT邦幫忙

2025 iThome 鐵人賽

DAY 5
0
Modern Web

JavaScript 進階修煉與一些 React ——離開初階工程師新手村的頭30天系列 第 5

離開 JS 初階工程師新手村的 Day 05|血統傳承:原型鏈 Prototype Chain

  • 分享至 

  • xImage
  •  
💡 本篇主題與重點字:**Prototype Chain**
- Prototype

定義

原型 Prototype
每個 JavaScript 的物件都有一個原型(使用__proto__ 存取或直接叫內部屬性 [[Prototype]])。這個原型本身也是一個物件,它也有自己的原型,如此串起來形成原型鏈

原型鏈 Prototype Chain
如果你訪問一個物件上不存在的屬性,JS 引擎就會沿著這個物件的原型鏈向上查找,直到找到該屬性或到達 null 為止,也就是達到此原型鏈的頂端。

範例

1. 手動設定原型 Object.create
https://ithelp.ithome.com.tw/upload/images/20250914/20168365LozutXzBBb.png

Object.create(animal) 創建了一個新的物件 dog,這個新物件的 [[Prototype]] 直接指向 animal,意味著這個新物件可以「看到」animal 上的所有方法和屬性。

在這邊的 dog__proto__ 指向 animal,因此可以使用父物件定義好的方法 animal.speak()

Object.create() 的好處

這邊使用 Object.create(Animal.prototype),好處在於它創建了一個「乾淨」的物件,這個物件沒有任何實例屬性,因此避免了呼叫 constructor 的副作用、原型鏈正確地指向 Animal.prototype
假設我們想要建立一個 sub Class Dog ,這樣的做法也允許我們安全地添加 Dog 特有的方法,而不會影響 Animal.prototype

2. 自定義建構函式 + 原型繼承
https://ithelp.ithome.com.tw/upload/images/20250914/20168365jHcVXHWRKP.png

當我們呼叫 myDog.speak() 時:

  1. JavaScript 引擎首先檢查 myDog 物件本身是否有 speak 方法 → 沒有
  2. 接著檢查 myDog.__proto__(也就是 Dog.prototype)是否有 speak 方法 → 沒有
  3. 再檢查 Dog.prototype.__proto__(也就是 Animal.prototype)是否有 speak 方法 → 找到了!
  4. 執行 Animal.prototype.speak,但是 this 仍然指向 myDog

偽類繼承

JavaScript 本質上是基於原型(prototype-based)的語言,而不是基於 class 的語言。在 JS 中,我們使用函數和原型來模擬傳統物件導向語言中的類繼承行為,這種做法就被稱為偽類繼承,如此處的 Animal.call(this, name)

讓我們來說文解字一下:Animal 實際上是一個函數,但我們把它當作 Class 來使用。在真正的 class-based language 中,類別是一個獨立的概念,但在 JavaScript 中,我們只是使用函數來模擬類別的行為,叫做「偽類」。而 Animal.call(this, name) 這行程式碼的作用是呼叫 Animal 函數,但將執行環境(this)設定為當前的 Dog 實例並傳入 name 參數,這樣做的效果等同於在 Dog 的 constructor 中執行了 this.name = name,也就是說,Dog 實例獲得了 Animal 中定義的屬性。這個子物件透過設定自己的原型、函數呼叫和 this 綁定的行為,模擬了真正類別「繼承」父物件的屬性與方法。

與方法繼承的區別
要注意的是,Animal.call(this, name) 只處理屬性的繼承,並不處理方法的繼承。方法的繼承是透過 Dog.prototype = Object.create(Animal.prototype) 這行來實現的。這種設計分離了屬性繼承(在建構函數中使用 call() 方法)和方法繼承(在原型鏈中設定繼承關係)。

當我們執行 Dog.prototype = Object.create(Animal.prototype) 時,我們實際上建立了這樣的原型鏈結構:

Dog 實例 → Dog.prototype → Animal.prototype → Object.prototype → null

原型繼承的好處

  1. 直接操作原型鏈:我們直接設定了 Dog.prototype 的原型指向,而不是複製方法或屬性
  2. 共享而非複製:所有 Dog 實例共享同一個 Animal.prototype 物件上的方法
  3. 動態查找機制:當我們呼叫 dog.speak() 時,JavaScript 引擎會沿著原型鏈查找這個方法

https://ithelp.ithome.com.tw/upload/images/20250916/20168365B8ICHgt31o.png

即使在創建實例之後,我們仍然可以給父類添加方法,而呼叫子類的方法,則會一層層沿著 prototype chain 往上找。

錯誤範例

1. 直接賦值
Dog.prototype = Animal.prototype;
這麼做的話,雖然 Dog 和 Animal 共享同一個 prototype 物件,但是當我們添加 Dog 特有的方法時,Animal 也會有這些方法。

2. 使用 new
Dog.prototype = new Animal();
這會執行 Animal 的建構函數,可能會有副作用,而且 Dog.prototype 會有不必要的實例屬性。

修正 constructor 指向

讓我們回顧剛剛的例子

https://ithelp.ithome.com.tw/upload/images/20250914/20168365jHcVXHWRKP.png

不知道讀者有沒有注意到這一行

Dog.prototype.constructor = Dog;

這句的用意是修正 constructor 指向。什麼意思呢?為什麼要這麼做?

我們再次思考當我們使用 Object.create() 時發生了什麼
當我們執行 Dog.prototype = Object.create(Animal.prototype) 時,實際上是用一個新的物件替換了 Dog.prototype。這個新物件是基於 Animal.prototype 創建的,所以它繼承了 Animal.prototype 的所有屬性,包括 constructor 屬性。

Object.create(Animal.prototype) 創建的新物件,其原型鏈指向 Animal.prototype。當我們存取 Dog.prototype.constructor 時,由於新的 Dog.prototype 物件本身沒有 constructor 屬性,JavaScript 引擎會沿著原型鏈向上查找,結果找到了 Animal.prototype.constructor,也就是 Animal

那你此時可能就會想,難不成每次寫 code 都要記得這句嗎?我們當然可以寫一個自定義繼承工具 function,使用 Object.create 手動指定原型,並重新指定 constructor,來避免忘記重新指向而導致非預期的行為與錯誤。
https://ithelp.ithome.com.tw/upload/images/20250916/20168365c5reXmwdgr.png


最後,附上昨天練習的解答

(1)

https://ithelp.ithome.com.tw/upload/images/20250916/201683657t5JtejftT.png

分析流程

步驟 執行內容 類型 加入佇列
1 console.log('1') 同步 -
2 setTimeout 註冊 宏任務 加入宏任務 queue1
3 Promise.then() 微任務 加入微任務 queue
4 console.log('5') 同步 -

微任務階段

  • 執行 promise.then:
    • 輸出:3
    • 再次註冊 setTimeout(4)→ 加入宏任務 queue2

宏任務階段

  1. 執行 queue1 → 輸出 2
  2. 執行 queue2 → 輸出 4

最終輸出

1
5
3
2
4

(2)

https://ithelp.ithome.com.tw/upload/images/20250915/20168365BfVTe9BXz2.png

分析流程

步驟 執行內容 類型 加入佇列
1 console.log('start') 同步 -
2 setTimeout 註冊 宏任務 加入宏任務 queue1
3 Promise.then() 微任務 加入微任務 queue
4 asyncFunc() 開始 同步 -
5 await → 產生微任務 微任務 加入微任務 queue
6 setTimeout 註冊 宏任務 加入宏任務 queue2
7 console.log('end') 同步 -

微任務階段

  • 執行 promise.then:
    • 輸出:promise 1
    • 輸出:promise 2
  • 執行 async await
    • 輸出:async end

宏任務階段

  1. 執行 queue1
    • 輸出 timeout 1
    • 執行 Promise → 加入微任務:timeout1-microtask
    • 清空微任務 → 輸出:timeout1-microtask
  2. 執行 queue2
    • 輸出 timeout 2

最終輸出

start
async start
end
promise1
promise2
async end
timeout1
timeout1-microtask
timeout2

上一篇
離開 JS 初階工程師新手村的 Day 04|時間迷宮:事件循環 Event Loop
下一篇
離開 JS 初階工程師新手村的 Day 06|繼承之力:super() 的英雄召喚
系列文
JavaScript 進階修煉與一些 React ——離開初階工程師新手村的頭30天10
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言