深入淺出原型(Prototype)與原型鏈(Prototype Chain)的概念與關係

學.誌|Chris Kang
12 min readMar 19, 2023
Photo by Amélie Mourichon on Unsplash

Prototype 一直是 JavaScript 這個語言的核心概念之一,但在一開始學習 JavaScript 時,其實一直沒有好好理解物件導向語言(OOP)的概念是什麼,以及如何應用。

這一次有機會比較完整地理解 JavaScript 的 Prototype 究竟是什麼,以及常聽見的 Prototype Chain 又是什麼,這一篇文章帶你理解。

什麼是 JavaScript 中的 Prototype?

在 JavaScript 中,每個對象都有一個隱藏的屬性 Prototype,這個屬性指向了該對象的原型(Prototype),而這個原型含括了共享的屬性和方法。例如我們使用的 Array、Object 時,之所以能使用 Build-in 的方法如 slice, map 等,也是因為透過繼承 Prototype,讓我們在新創建的 Array 物件,能擁有這些方法或屬性。

在理解 Prototype 的概念,我們可以把 Prototype 想像成一個藍圖,在 OOP 中被稱為 Class。透過這個藍圖,我們可以不停生成許多實例化的物件。在 OOP (Object-Oriented Programming)中,生成的物件則被稱為 Instance。

如何創建 Prototype?

要在 JavaScript 裡建立 Prototype,總共有三個方式可以達成:

  1. 函式建構式(Function Constructor):從 function 生成這一些 object,像是 Array 或 Map 就是這樣生成的。
  2. ES6 的 Class:其實就是前面建構式的語法糖,和第一個方式做的事情一摸一樣。
  3. Object.create():是最直接的方式把物件跟 Prototype 串在一起,但實務上比較少用。
// Function Constructor
function Person(name) {
this.name = name;
}

Person.prototype.sayHi = function () {console.log(`My name is ${this.name}`)}

// Class
class Person {
constructor {
this.name = name;
}

sayHi() {
console.log(`My name is ${this.name}`)
}
}


// Object.create()
var Person = {
name: this.name,
sayHi: function(){
return console.log(`My name is ${this.name}`);
}
}

var Jay= Object.create(Person);
console.log(Jay);

其中,如果是建構式的 function,通常都會以大寫開頭避免混淆。另外因為 arrow function 沒有自己的 this,所以不能用在此處。在這裡使用 arrow function,會直接指向 windows。

而 Class 基本上只是函式建構式的語法糖,能夠做到和函式建構式一模一樣的目標,但能夠以更簡潔而明確地方式呈現。因此此處皆以函式建構式為主進行說明,後續會有另一篇文章特別說明 Class 的寫法。

函式建構式(Function Constructor)

函式建構式是 ES6 以前的方法,他並非一開始就被 JavaScript 設計出來,但因 JavaScript 設計時一樣遵循 OOP 的建構模式,因此所有在 JavaScript 裡面存在的函式、參數等都擁有 Prototype 這個隱藏屬性,這也才讓我們有辦法使用建構式來建立 Prototype。

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

Person.prototype.sayHi = function() {
console.log('Hello, my name is ' + this.name);
}

// 使用 new 來實例化建構函式
const person1 = new Person('Jay');
const person2 = new Person('Chris');

person1.sayHi(); // Hello, my name is Jay
person2.sayHi(); // Hello, my name is Chris

在這段程式碼中,我們定義了一個 Person 的函式,它接受一個 name 的參數並將其儲存在建構式中。接著,我們在建構式外的 Prototype 中,定義了一個 sayHi 的方法,這個方法會在 console 顯示 Hello, my name is “name”。因為這個方法透過 new 來實例化,因此他們都具有相同的 Person 原型,也就是 Person.prototype,對於 person1 和 person2 都可以使用 sayHi 的方法。

記憶體浪費的問題

其中,雖然建構式 Person 當中一樣可以直接放入方法 this.sayHi,但因為每一次的實例化都需要複製一模一樣的方法,這會導致記憶體的浪費。因此若要對特定的建構式新增方法,一律都使用上述的 Person.methodName 來新增方法。

確認繼承物件狀態的方法

針對確認物件的繼承狀態,我們可以使用 instanceof : console.log(jay instanceof Person) ,用以確認該物件是否繼承自特定的原型。也可以使用isPrototypeOf,來相反檢驗是否為其物件的原型。

物件的 Prototype 重要概念

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

Person.prototype.sayHi = function() {
console.log('Hello, my name is ' + this.name);
}

// 使用 new 來實例化建構函式
const person1 = new Person('Jay');
const person2 = new Person('Chris');

person1.sayHi(); // Hello, my name is Jay
person2.sayHi(); // Hello, my name is Chris

繼續使用上述的 Sample Code,在這個情境下,Person.prototype 指的是 Person 函式物件所擁有的屬性和方法,而非 Person 物件本身的 Prototype。因為在這個情況,Person 的 Prototype 其實是 Function.prototype

也就是說,Person.prototype 是一個物件,它包含了一些屬性和方法,可以被 Person 的實例所共享。當我們建立一個 Person 物件的實例時,JavaScript 引擎會將這個實例的 Prototype 屬性設定為 Person.prototype

這個 prototype 屬性是一個物件,它被用來建立由這個函式建立的物件的原型。這個 prototype 屬性在函式建立時就被建立,並且默認為一個空物件,只有一個 constructor 屬性指向這個函式本身。

換句話說,如果使用 Person.prototype.constructor ,其實會回到 Person 自己本身。

進一步瞭解物件如何被建構

把整個 Prototype 實例化的過程完整描述,會有四個步驟:

  1. 建立一個全新的 Object 並命名
  2. 將 this keyword 指派給傳入的參數,例如 name: 'Chris'
  3. 將創建之新物件的 __proto__ 屬性,link 到建構式的 prototype 這個屬性,意指 Person.prototype
  4. 新的物件從建構式中被傳回,除非我們特別使用 return 來回傳特定值。

這樣一來,這個實例就可以存取 Person.prototype 中的屬性和方法。因此,Person.prototype 扮演的角色是作為 Person 的原型,提供了一些可被 Person 實例所共享的屬性和方法。

因此很重要的觀念,可以用下面這一行 Code 來解釋:

console.log(jay.__proto__ === Person.prototype) // true

此處 jay.__proto__ 指的是 jay 這個物件所參考的 prototype,與 Person 能夠共享的 Prototype — Person.prototype 正是同一個。

原型鏈(Prototype Chain)是什麼?

當原先建構出來的物件沒有所屬的方法或屬性,JavaScript 就會往 prototype 去找該方法或屬性,而這個把新建的物件與 prototype 繼承的過程,被稱為 Prototype Inheritance / Delegation。

因為這個過程中,查找的是 prototype 這一個共用的屬性和方法,不需要在每一個物件都創建一個相同的方法,因此能夠節省掉重複使用的記憶體,也因此說明為何不要在建構式當中,直接固定寫入方法。

原型鏈(Prototype Chain)的核心概念

Prototype Chain 指的是當無法在原物件本身找到相對應的方法,JavaScript 就會往 prototype 去找尋相對應的方法或屬性。如果沒有找到,就會繼續向上查找:

  1. 搜尋原物件本身的方法或屬性
  2. 若找不到,則往他繼承的 Prototype 來搜尋
  3. 若還是找不到,則會往創建這個 Prototype 的 Prototype 來搜尋,這裡是 Function.prototype
  4. 若還是沒有,則會繼續往上查找,最後找的的是 Object.prototype
  5. 在最後指向 Object.prototype 的 Prototype 會指向 null,此時 Prototype Chain 就會停下

因為每一個物件都一定有 prototype,因此 Person.prototype 的 Prototype 指的是 Function.prototype,意指創建的是 Function 的 build-in 建構式(例如 const obj = {}new Object())。

此處的 null 是什麼意思?

此處補充說明,當一個對象的原型對象為 null 時,它就不再繼承自任何原型對象。也就是說,它不再具有任何屬性或方法,因為所有的屬性和方法都是從其原型對象中繼承而來。

因此,當我們將 Object.prototype 的原型對象設置為 null 時,它就不再具有任何屬性或方法,也就失去了其作為原型對象的功能,最終指向 null。

範例一

下列 Code 的整個原型鏈如下,其搜尋過程為:

jay.hasOwnProperty('name') 
  1. 在 jay 這個物件中找不到 hasOwnProperty 這個方法,因此往下一層找
  2. Person.prototype 中找不到 hasOwnProperty 這個方法,因此往下一層找
  3. 在下一層找 Function.prototype 中一樣找不到,因此再下一層
  4. Object.prototype 中找到 hasOwnProperty 這個方法,並執行該方法

範例二

// Animal 建構式
function Animal(name) {
this.name = name;
}

// 將 walk 方法加入 Animal 的 prototype 中
Animal.prototype.walk = function() {
console.log(this.name + ' is walking.');
}

// 將 Animal 原有的屬性綁定,透過 this (指的是 Bird 生成後的物件) 綁定至此處的 Bird
function Bird(name) {
Animal.call(this, name);
}

// 使用 Object.create 將 Animal 的 prototype 繼承至 Bird,並將此處的建構式指定為上述的 Bird
Bird.prototype = Object.create(Animal.prototype); // 會生成一個全新的 prototype 避免污染
Bird.prototype.constructor = Bird;

// 給 Bird 加上方法
Bird.prototype.fly = function() {
console.log(this.name + ' is flying.');
}

// 實例化 Bird
const bird = new Bird('Eagle');

// 顯示結果
console.log(bird.name); // Eagle
bird.walk(); // Eagle is walking.
bird.fly(); // Eagle is flying.

在上述的範例中,我們通過 console.log(bird.name) 訪問了 bird 對象的 name 屬性,這個屬性實際上是保存在 Animal 對象中的,但是由於 Bird 對象繼承了 Animal 對象的原型,因此可以在 bird 對象中訪問到。

接著,我們通過 bird.walk()bird.fly() 訪問了 bird 對象的 walk 方法和 fly 方法,這兩個方法分別保存在 Animal.prototypeBird.prototype 對象中。

當我們訪問 bird.walk() 時,JavaScript 引擎會首先在 bird 對象中尋找 walk 方法,找不到就到 Bird.prototype 對象中尋找,再找不到就到 Animal.prototype 對象中尋找,最終找到了 walk 方法。

同樣,當我們訪問 bird.fly() 時,JavaScript 引擎會首先在 bird 對象中尋找 fly 方法,找不到就到 Bird.prototype 對象中尋找,找到了 fly 方法。

原型與原型鏈––總結

原型和原型鏈是 OOP 在 JavaScript 的實際呈現,因此瞭解其中的邏輯與設計概念,能夠很好地處理和解決不同物件之間的繼承狀態。

瞭解這些概念,對於助於我們更好地理解和使用 JavaScript 會很有幫助。如果有任何說明不清楚的地方,或是理解上有錯誤,都歡迎讀者一起留言討論!

--

--

學.誌|Chris Kang

嗨!我是 Chris,一位擁有技術背景的獵頭,熱愛解決生活與職涯上的挑戰。專注於產品管理/資料科學/前端開發 / 人生成長,在這條路上,歡迎你找我一起聊聊。歡迎來信合作和交流: chriskang0917@gmail.com