前情
要講原型前就要先提到 OOP(物件導向程式設計),OOP 的基本概念是一種將現實世界中的事物抽象為類別(Class) 的程式設計方式,每個類別描述了某類事物的屬性與行為,而物件(Object) 則是根據類別建立出的實體,擁有真正具體的資料與功能。
藉由類別代表其最重要的概念或特質或資料或功能,接著根據這個「類別」建立物件實體 (Object instance) — 即該物件包含了類別中所定義的屬性與行為。 而物件的建立過程通常是透過執行類別的建構子(constructor)函式完成的,並自動帶入類別中定義的屬性與方法。
雖然在 ES6 中 JavaScript 引入了 class
語法,但本質上它仍是使用「原型(prototype)」來實作物件導向。這與傳統的基於類別(class-based)語言不同,JavaScript 採用的是「基於原型的繼承(prototype-based inheritance)」。換句話說,JavaScript 的每個物件都可以指向另一個物件作為原型,並從中繼承屬性與方法。
原型是什麼?
我們可以把原型想像成一個藍圖。例如我們想建一台車,設計一台車的藍圖就會包括以下的特性:
- 車的種類 (電動車、貨車、摩托車...)
- 車的顏色 (紅色、黑色、藍色...)
- 車的坐位數目
但它只是一個藍圖,並不是實實在在的一台車。所以要實際建出一台車,就可以按這份藍圖去建起來,並且要建什麼車,就依據藍圖的基礎,給予不同的調整:
- 我的車子跟你的車子無關係,是兩個不同的物件,各自按自己的喜好調整屬性值
- 我的車子和你的車子繼承了同一個藍圖,它們的內部結構都指向同一個原型對象,因此可以共享藍圖中的方法
在這個例子中,Car.prototype
就是那份「藍圖」,而 myCar
是根據它產生的實體車。
原型鍊 (prototype chain)
🧬 原型鍊(Prototype Chain)
基本概念
在 JavaScript 中,物件之間的繼承是透過「原型」(Prototype)來實現的,這種繼承機制稱為原型繼承(Prototypal Inheritance)。
每個 JavaScript 物件在建立時,會有一個隱藏屬性 [[Prototype]]
(在大多數瀏覽器中可透過 __proto__
存取),它指向另一個物件,也就是它的原型。這種層層相連的結構就叫做「原型鍊」。
上例中 child 雖然本身沒有 greeting
屬性,但 JavaScript 會沿著原型鍊向上查找,發現在 child.__proto__
(也就是 parent)中有此屬性,於是就返回了 'hello'
。
🔍 查找屬性的流程
當你存取一個物件的屬性時(如 obj.prop
),JavaScript 會依以下順序尋找:
- 先找
obj
本身是否有prop
屬性 - 沒有的話,往
obj.[[Prototype]]
(即obj.__proto__
)找 - 再往
obj.[[Prototype]]
的[[Prototype]]
… 一路向上查找 - 若查到最終原型是
null
(即Object.prototype
的原型,也就是原型鍊的終點),仍找不到prop
屬性,則回傳undefined
這樣的機制讓多個物件可以共用屬性與方法,避免重複定義,也使得 JavaScript 的繼承更具彈性。
🧱 prototype
與 __proto__
的差異
建構式的 prototype
與實體的 __proto__
prototype
是屬於「函式」的屬性,它是一個物件,用來定義該函式作為建構函式(Constructor)產生的實體的原型__proto__
是每個物件的內部屬性,指向它的原型,也就是那條「原型鍊」的上層節點
函式建構式 (function constructor)
在 JavaScript 中,除了使用物件字面量({}
)建立物件外,我們也可以透過「函式建構式(Function Constructor)」來建立物件實體。這種方式通常搭配 new
關鍵字來使用。
new
關鍵字
當你執行 new Person()
,JavaScript 在背後會進行以下幾個步驟:
- 建立一個全新的空物件:
{}
- 將這個物件的
__proto__
屬性設為Person.prototype
(指向原型) - 執行
Person
函式,並將this
綁定到這個新建立的物件上 - 如果建構式中有明確
return
一個物件,則回傳該物件;否則回傳上述新建立的物件
透過參數創建多個不同的物件
每次使用 new
呼叫建構式時,都可以傳入不同的參數,並將這些值指定給 this
上的屬性,藉此來建立具有相同結構但屬性值不同的實體物件。
這種方式就像是用一個模板快速「工廠化」地建立出多個類似的物件。
return 的例外情況
在函式建構式中,通常不需要寫 return
。不過:
return
一個物件,則會覆蓋預設的回傳值(即this
)return
一個基本型別(如字串、數字等),則會被忽略,仍回傳this
函式建構子裏面有一些屬性:
- 函式裏有
prototype
屬性,是一個空物件 - 這個
prototype
屬性裏面,再有 2 個屬性:constructor
屬性 (指回這一層它自己的建構函式,A.prototype.constructor === A
)__proto__
屬性 (再找上一層的原型 (prototype)object
)
constructor
屬性,就是指回自己這個函式建構子的本身,即是 car
這個函式。__proto__
屬性就是這個函式再上一層的原型,就是 object
,因為 function
是屬於 object
型別。
實體物件的特別之處:
- 物件裏有
__proto__
,裏面有:constructor
屬性 (指向上一層它的建構函式)__proto__
屬性 (再找上一層的原型 (prototype) )__proto__
裏面還要包多一個__proto__
,這裏的__proto__
是指向它的原型,就是原型
的prototype
。
所以實體.__proto__ === 原型.prototype
- 它們都有同一個
constructor
屬性,因為它們都是由同一個函式建構子產生的。
實體物件只有__proto__
屬性,不同於建構函式同時擁有__proto__
和prototype
這兩個屬性。實體物件的__proto__
,會指向上一層的原型,即上一層的prototype
,這個上一層的prototype
會放著:
constructor
(指回這一層它自己的建構函式)__proto__
(再找上一層的原型 (prototype) )- 一些之前定義好的方法 (如有)