September 25, 2023

Viiisit [JavaScript] - Prototype, Prototype Chain & Prototype Inheritance in JavaScript!

#javascript# prototype

今天來點 JavaScript 的原型與繼承!

Prototype(原型)是什麼?

當我們在 JavaScript 中建立物件時,每個物件都有一個隱含屬性 [[Prototype]]
叫做 prototype(原型)。原型就像是物件的「模板」,決定了物件的一些基本特性和方法。

藉由以下例子,來看看 prototype__proto__

假設我們有一個建構式叫做 Person,我們用來建立一個人的物件。
這個 Person 建構函式有一個原型且此原型包含了一些方法和屬性。

1
2
3
4
function Person() { }

// 透過 prototype 方式,查看 Person 的原型
console.log(Person.prototype); // {constructor: ƒ}

當你在 console 印出 console.log(Person.prototype),你會看到:

1
2
3
{constructor: ƒ}
constructor: ƒ Person()
[[Prototype]]: Object

以上其實就顯示了 Person.prototype 物件的屬性和原型鏈。

{constructor: ƒ}:這表示 Person.prototype 物件本身的屬性。
在這裡,你看到一個 constructor 屬性,指向建構子 Person
這個屬性告訴 JavaScript,用來建立這個物件的建構子是 Person

constructor: ƒ Person():這是 constructor 屬性的具體內容。
顯示了 constructor 是一個函數,函數名稱是 Person
這表示這個物件是由 Person 建構函數建立的。

[[Prototype]]: Object:這表示 Person.prototype 物件的原型鏈。
Person.prototype 的原型是 Object
這是因為在 JavaScript 中,幾乎所有物件都繼承自 Object,因此 Object 是原型鏈的頂端。

在這個範例中,Person.prototype 繼承自 Object.prototype
constructor 屬性告訴我們與之關聯的建構子是 Person


接著,我們看一下透過 Person,建立一個 person1 物件:

1
2
3
4
5
6
function Person() { }

const person1 = new Person();

// 透過 __proto__ 方式,查看 person1 的原型
console.log(person1.__proto__); // {constructor: ƒ}

當我們使用 new Person() 建立一個具體的人的物件時,
這個物件會自動連接到 Person 建構函數的原型。
這意味著這個人的物件可以繼承(就像繼承父母的特徵一樣)原型中的方法和屬性。

這個概念有點像家族。
建構函式就像是家族的創始人,原型就像是家族的傳統,而物件就像是家族的成員,他們可以繼承和分享這些傳統。

透過 person1.__proto__Person.prototype 存取原型時,
你會看到一個對象,其中包含了 constructor 屬性,指向建立這個物件的建構子。

__proto__ 是一個非標準的方法,
通常用於訪問物件的原型,可以用来直接訪問和設置物件的原型。
注意!, __proto__ 方法並不在 ECMAScript 規範中,
實際上要取得物件的原型會使用 Object.getPrototypeOf()


Prototype Chain(原型鏈)是什麼?

剛剛在 Person.prototype 有提及到原型鏈的概念,在這我們再一次加深印象!

Prototype Chain(原型鏈)是 JavaScript 中實作繼承和共享屬性/方法的重要機制。
原型鏈是一個由原型物件連接起來的鏈條,決定了在 JavaScript 中尋找屬性和方法時的搜索順序。
當你試圖呼叫一個物件的屬性或方法時,如果該物件本身沒有這個屬性或方法,
JavaScript 會沿著原型鏈向上查找,直到找到對應的屬性或方法,
或者達到原型鏈的末端 Object.prototype

Object.prototype,這是 JavaScript 中的所有物件原型的根。
如果在整個原型鏈上都找不到匹配的屬性或方法,JavaScript 將停止搜尋並傳回 undefined。

因此,透過剛剛建立的 Person 函式,再次觀察物件 person1 與建構函式 Person 之間的原型關係。

1
2
3
4
5
6
7
8
function Person() { }

const person1 = new Person();

// person1 物件可以透過 __proto__ 方法訪問到他的原型
person1.__proto__ === Person.prototype; // true
Object.getPrototypeOf(person1) === Person.prototype; // true
person1.__proto__ === Object.getPrototypeOf(person1); // true

Prototype Inheritance(原型繼承)是什麼?

原型繼承是 JavaScript 中的一種繼承機制,允許一個物件繼承另一個物件的屬性和方法。
基於原型鏈的概念,子層物件的原型指向父層物件,從而實現屬性和方法的共享和繼承。

讓我們來看看以下例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// 定義爸爸層 Person
function Person(name) {
this.name = name;
}

// 在爸爸層原型上添加方法
Person.prototype.sayHello = function () {
console.log("Hello, my name is " + this.name);
};

// 定義一個兒子層 Student
function Student(name, studentId) {
// 使用爸爸層的構造函數,並傳遞名字
Person.call(this, name);
// 定義一個新的屬性,只有 Student 有的屬性
this.studentId = studentId;
}

// 創建兒子層的原型鏈,繼承爸爸層的方法
Student.prototype = Object.create(Person.prototype);

// 指向自己的構造函數
Student.prototype.constructor = Student;

// 在兒子層原型上添加自己的 study 方法
Student.prototype.learn = function () {
console.log(this.name + " is learning.");
};

// 創建爸爸層實例
var person = new Person("Viii");
person.sayHello(); // Hello, my name is Viii

// 創建兒子層實例
var student = new Student("Viii", "001");
student.sayHello(); // Hello, my name is Viii
student.learn(); // Viii is learning.
console.log(student.studentId); // 001

關於指向自己的構造函數:Student.prototype.constructor = Student;
如果建立一個 Student 實例,並嘗試訪問其建構函數,
將返回 Person 而不是 Student,這可能會導致混淆和不正確的行為。
為了更正這個問題,需要手動將 Student.prototype.constructor 設置為 Student,以確保指向正確的建構函數。這樣,當你建立 Student 的實例時,建構函數引用將正確指向 Student


Brief Summary


References:
MDN
原型繼承與原型鏈 |ALPHA Camp
JS 原力覺醒 Day22 - 原型共享與原型繼承
原型基礎物件導向
[教學] JavaScript Prototype (原型) 的用法