原型 & 原型鏈

原型 & 原型鏈

前情

要講原型前就要先提到 OOP(物件導向程式設計),OOP 的基本概念是一種將現實世界中的事物抽象為類別(Class) 的程式設計方式,每個類別描述了某類事物的屬性與行為,而物件(Object) 則是根據類別建立出的實體,擁有真正具體的資料與功能。

藉由類別代表其最重要的概念或特質或資料或功能,接著根據這個「類別」建立物件實體 (Object instance) — 即該物件包含了類別中所定義的屬性與行為。 而物件的建立過程通常是透過執行類別的建構子(constructor)函式完成的,並自動帶入類別中定義的屬性與方法。

雖然在 ES6 中 JavaScript 引入了 class 語法,但本質上它仍是使用「原型(prototype)」來實作物件導向。這與傳統的基於類別(class-based)語言不同,JavaScript 採用的是「基於原型的繼承(prototype-based inheritance)」。換句話說,JavaScript 的每個物件都可以指向另一個物件作為原型,並從中繼承屬性與方法。

原型是什麼?

我們可以把原型想像成一個藍圖。例如我們想建一台車,設計一台車的藍圖就會包括以下的特性:

  • 車的種類 (電動車、貨車、摩托車...)
  • 車的顏色 (紅色、黑色、藍色...)
  • 車的坐位數目

但它只是一個藍圖,並不是實實在在的一台車。所以要實際建出一台車,就可以按這份藍圖去建起來,並且要建什麼車,就依據藍圖的基礎,給予不同的調整:

  • 我的車子跟你的車子無關係,是兩個不同的物件,各自按自己的喜好調整屬性值
  • 我的車子和你的車子繼承了同一個藍圖,它們的內部結構都指向同一個原型對象,因此可以共享藍圖中的方法
function Car(type, color, seats) {
  this.type = type;
  this.color = color;
  this.seats = seats;
}

Car.prototype.drive = function () {
  console.log("Vroom!");
};

const myCar = new Car("電動車", "黑色", 5);
myCar.drive(); // 繼承自 Car.prototype

在這個例子中,Car.prototype 就是那份「藍圖」,而 myCar 是根據它產生的實體車。

原型鍊 (prototype chain)

🧬 原型鍊(Prototype Chain)

基本概念

在 JavaScript 中,物件之間的繼承是透過「原型」(Prototype)來實現的,這種繼承機制稱為原型繼承(Prototypal Inheritance)。 每個 JavaScript 物件在建立時,會有一個隱藏屬性 [[Prototype]](在大多數瀏覽器中可透過 __proto__ 存取),它指向另一個物件,也就是它的原型。這種層層相連的結構就叫做「原型鍊」。

一個物件裡面除了所給予的屬性值外,另外也包含原型 prototype

const parent = { greeting: 'hello' };
const child = Object.create(parent);

console.log(child.greeting); // 'hello'

上例中 child 雖然本身沒有 greeting 屬性,但 JavaScript 會沿著原型鍊向上查找,發現在 child.__proto__(也就是 parent)中有此屬性,於是就返回了 'hello'

🔍 查找屬性的流程

當你存取一個物件的屬性時(如 obj.prop),JavaScript 會依以下順序尋找:

  1. 先找 obj 本身是否有 prop 屬性
  2. 沒有的話,往 obj.[[Prototype]](即 obj.__proto__)找
  3. 再往 obj.[[Prototype]][[Prototype]]… 一路向上查找
  4. 若查到最終原型是 null(即 Object.prototype 的原型,也就是原型鍊的終點),仍找不到 prop 屬性,則回傳 undefined

這樣的機制讓多個物件可以共用屬性與方法,避免重複定義,也使得 JavaScript 的繼承更具彈性。

🧱 prototype__proto__ 的差異

  • prototype 是屬於「函式」的屬性,它是一個物件,用來定義該函式作為建構函式(Constructor)產生的實體的原型
  • __proto__每個物件的內部屬性,指向它的原型,也就是那條「原型鍊」的上層節點
function Car() {}
const myCar = new Car();

console.log(myCar.__proto__ === Car.prototype); // true
console.log(myCar instanceof Car); // true

函式建構式 (function constructor)

在 JavaScript 中,除了使用物件字面量({})建立物件外,我們也可以透過「函式建構式(Function Constructor)」來建立物件實體。這種方式通常搭配 new 關鍵字來使用。

new 關鍵字

function Person(name, age) {
  this.name = name;
  this.age = age;
}

const john = new Person('John', 30);
console.log(john.name); // 'John'
console.log(john instanceof Person); // true

當你執行 new Person(),JavaScript 在背後會進行以下幾個步驟:

  1. 建立一個全新的空物件:{}
  2. 將這個物件的 __proto__ 屬性設為 Person.prototype(指向原型)
  3. 執行 Person 函式,並將 this 綁定到這個新建立的物件上
  4. 如果建構式中有明確 return 一個物件,則回傳該物件;否則回傳上述新建立的物件

透過參數創建多個不同的物件

const alice = new Person('Alice', 28);
const bob = new Person('Bob', 35);

console.log(alice.name); // 'Alice'
console.log(bob.name);   // 'Bob'

每次使用 new 呼叫建構式時,都可以傳入不同的參數,並將這些值指定給 this 上的屬性,藉此來建立具有相同結構但屬性值不同的實體物件。

這種方式就像是用一個模板快速「工廠化」地建立出多個類似的物件。

return 的例外情況

在函式建構式中,通常不需要寫 return。不過:

  • return 一個物件,則會覆蓋預設的回傳值(即 this
  • return 一個基本型別(如字串、數字等),則會被忽略,仍回傳 this
function Weird() {
  this.name = 'Default';
  return { name: 'Override' }; // 會 return 這個物件,而非預設的 this
}

const obj = new Weird();
console.log(obj.name); // 'Override'

函式建構子裏面有一些屬性:

  • 函式裏有 prototype 屬性,是一個空物件
  • 這個 prototype 屬性裏面,再有 2 個屬性:
    1. constructor 屬性 (指回這一層它自己的建構函式,A.prototype.constructor === A)
    2. __proto__ 屬性 (再找上一層的原型 (prototype) object)

constructor 屬性,就是指回自己這個函式建構子的本身,即是 car 這個函式。__proto__ 屬性就是這個函式再上一層的原型,就是 object,因為 function 是屬於 object 型別。

實體物件的特別之處:

  • 物件裏有 __proto__,裏面有:
    1. constructor 屬性 (指向上一層它的建構函式)
    2. __proto__ 屬性 (再找上一層的原型 (prototype) ) __proto__裏面還要包多一個__proto__,這裏的 __proto__ 是指向它的原型,就是原型prototype
      所以 實體.__proto__ === 原型.prototype
    3. 它們都有同一個 constructor 屬性,因為它們都是由同一個函式建構子產生的。

實體物件只有__proto__屬性,不同於建構函式同時擁有__proto__prototype這兩個屬性。實體物件的__proto__,會指向上一層的原型,即上一層的prototype,這個上一層的prototype會放著:

  • constructor (指回這一層它自己的建構函式)
  • __proto__ (再找上一層的原型 (prototype) )
  • 一些之前定義好的方法 (如有)

https://i.imgur.com/brNPVuV.png