在 JavaScript 中,每個物件都有原型,物件可以從原型繼承屬性和方法,實現程式碼重用。而被繼承的原型物件也可以繼承其他物件,這樣一層又一層被繼承的屬性與方法就形成了原型鏈。
什麼是原型
每個物件都有一個隱藏屬性 [[Prototype]],用來指向該物件的原型物件。原型讓物件可以繼承其他物件的屬性與方法。
- 讓物件可以繼承屬性與方法
- 建立物件之間的關聯
- 實現程式碼重用
- 支援 JavaScript 的繼承機制
看起來是不是很像其他語言的 class? 在 JavaScript 中,class 本質上其實只是 prototype 的語法糖。
屬性查找機制
當你存取物件屬性的時候, JavaScript 會先從自身開始找,找不到這個屬性會往 [[Prototype]] 找,再找不到就繼續往上。這條路徑也是我們開頭說的原型鏈。
// 查找屬性步驟
1. 找物件自己
2. 往 [[Prototype]] 找
3. 往原型物件的 [[Prototype]] 繼續找
4. 直到找到屬性或是到 null
Prototype 存取方式
__proto__
obj.__proto__
getPrototypeOf/setPrototypeOf
Object.getPrototypeOf(obj)
Object.setPrototypeOf(obj, proto)
Prototype 怎麼繼承
__proto__ 範例
首先建立兩個不相關的物件,一個叫 animal, 一個叫 dog。animal 與 dog 的屬性完全不同。
// 宣告物件 animal
const animal = {
run: true,
eat(){
console.log("eat!")
}
};
const dog = {
jump: true
};
可以先測試看看 dog.run,因為 dog 沒有 run 這個屬性,因此會印出 undefined。
console.log(dog.run); // undefined
接著用 __proto__ 設定 dog 的原型為 animal:
dog.__proto__ = animal;
再操作一次,dog 就可以使用 animal 的屬性與方法了
console.log(dog.run); // true
dog.eat(); // eat!
你也可以再建立更深層的繼承:
// 再建立一個物件,直接將 dog 賦值給 collie 的 __proto__ 屬性
const collie = {
__proto__: dog
};
原型鏈會像:collie → dog → animal → Object.prototype → null。
大多數物件的原型鏈最終都會到達Object.prototype,它的[[Prototype]]是null,代表鏈條結束。
建構函式與 new
還記得我們在【JavaScript 入門:物件 (Object )是什麼、要怎麼建立?】就悄悄提過的原型嗎?在介紹建立物件的方法中,我們介紹了「建構函式 + new」的做法,一起來回顧一下當時的程式:
建構函式
// 定義建構函式
function Person(name, age, height, weight){
// 這邊的 this 指的是即將被 new 出來的物件
this.name = name;
this.age = age;
this.height = height;
this.weight = weight;
this.sayHi = function(){
console.log("Hi");
}
}
// 使用 new 運算子建立 instance
const person1 = new Person("Elaine", 18, 173, 50);
const person2 = new Person("Tom", 27, 183, 78);
console.log(person1.sayHi === person2.sayHi); // false
這個例子中,我們把 sayHi 方法寫在建構函式中,這代表每 new 一個新物件,物件都有各自的 sayHi,及使用法一模一樣、也出自同一個建構函式。
new 關鍵字
在接著,我們又說了 new 在背後做的事情其實是:
- 建立空物件
- 將新物件的
[[Prototype]]隱藏屬性指向建構函式的prototype屬性 - 將 this 綁定到這個物件
- 執行函式的內容
- 回傳物件
眼尖的你一定馬上發現了,重點在於第二步的「將新物件的 [[Prototype]] 隱藏屬性指向建構函式的 prototype 屬性」。
prototype 屬性
建構函式的 prototype 屬性是 function 準備給未來 instance 的原型物件,當使用 new 運算子建立 instance 時,new 會讓他們接起來,也就是:
new會把 instance 的[[Prototype]]指向 constructor.prototype
聰明的你一定馬上聯想到了,「物件可以繼承原型的屬性與方法」,所以你會知道,物件共用的方法可以這麼設定:
function Person(name, age, height, weight){
this.name = name;
this.age = age;
this.height = height;
this.weight = weight;
}
// 把方法定義在 Person 的 prototype 屬性上,後面 new 出來的物件都可以繼承
Person.prototype.sayHi = function(){
console.log("Hi");
}
const person1 = new Person("Elaine", 18, 173, 50);
const person2 = new Person("Tom", 27, 185, 80);
console.log(person1.sayHi === person2.sayHi); //true
示意圖
// 方法寫在 constructor 內
p1
├─ name
└─ sayHi (一份)
⭡
p2 不一樣
├─ name ↓
└─ sayHi (另一份)
// 方法寫在 constructor 的 prototype 屬性內
p1 p2
├─ name ├─ name
↓ ↓
[[Prototype]]
└─ sayHi (共用一份)
補充 1:因為 function 本身也是物件,所以他才會有 prototype 屬性唷!
補充 2:prototype 物件本身有一個 constructor 屬性會指回建構函式,這讓整個結構形成閉環console.log(Person.prototype.constructor === Person); // true
總結
在這篇文章中出現了許多相似的屬性,我在第一次看得時候也覺得有夠複雜有夠難,但是多翻幾篇文章好像有漸漸變得清楚,幫大家快速整理這些相似的屬性:
[[Prototype]]:每個物件的隱藏屬性,指向物件的原型物件,是真正決定屬性繼承與原型鏈的機制。prototype:只有建構函式才有的屬性,本身也是一個物件,用來存放將要被 new 出來的實例共享的屬性與方法。__proto__:幾乎所有物件都有的存取器屬性,可用來讀寫物件的原型連接。getPrototypeOf/Object.setPrototypeOf:[[Prototype] 的存取器。
物件繼承並不是複製屬性,而是透過原型鏈查找屬性,因此共享方法可以有效節省記憶體。
參考














