原型鏈(Prototype Chain)

JavaScript 物件是一「包」動態的屬性(也就是它自己的屬性)並擁有一個原型物件的鏈結,當物件試圖存取一個物件的屬性時,其不僅會尋找該物件,也會尋找該物件的原型、原型的原型……直到找到相符合的屬性,或是到達原型鏈的尾端(Object.prototype)。

簡單來說:

當物件想要存取自身沒有的屬性時,他會一直往上找到為止就是原型鏈

物件導向(Object-oriented programming;OOP)

物件導向是一種程式設計模式,物件是其中的最基本單位,並且軟體是由無數的物件交互運作而成,而JS就支援這樣的模式並且原型鏈的原理必須從這邊理解

  • 類別(class)

擁有 class, instance的概念,class會定義物件的屬性,instance則是由被定義的屬性產生的物件,Java, C++使用類似的概念

  • 原型(prototype)

沒有類別跟實體的概念,創立的物件會以原型為範本來繼承屬性,如JS

建構函式與實例 Constructor & Instance

Car 其實只是一個普通的函式,但如果你用 new 運算子來呼叫它的話,JavaScript 就會將它視為建構函式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function Car(wheel, door, fuel) {
this.wheel = wheel,
this.door = door,
this.fuel = fuel
};

let truck = new Car(6, 2, "柴油");

// console.log(truck) 的印出結果
Car {
door: 2
fuel: "柴油"
wheel: 6
__proto__: Object
}

你會發現 Car 確實依據我們傳入的參數把 truck 的相關屬性給設定好了,而且在前面標註了 Car,以此說明 truck 是 Car 的實例

原型 prototype

prototype是一個隱藏的內建屬性,在JS中每個函式都會有,而建構函式也是函式,當然就也有 prototype

這邊我們來印出console.log(Car.prototype);會得到

印出兩個部分:

  • constructor

這邊就是建構函式的內容物

Car.prototype.constructor === Car 會印出true

  • __proto__

在 JavaScript 裡,每個物件型別的變數都有 __proto__

印出console.log(truck.__proto__);

會得到跟Car.prototype一樣的結果

從這邊可以明白,truck作為Car的instance它繼承了Car的屬性,證明方法如下:

1
console.log(truck.__proto__ === Car.prototype); // true 它們兩個指向同一個物件

new 運算子

new 背後做的事情不是很複雜但卻很重要,它將instance以及prototype之間建立了連結。

創造instance時會發生:

  1. instance會初始化,並可以透過建構函式新增屬性
  2. instance的__proto__跟建構函式的prototype是一樣的

這邊透過函式使用this設定屬性非常奇怪,this.屬性這樣的方式做添加,不太合理,因為照理來說這樣會加到全域的屬性上面,所以關鍵出在new

其實一切的關鍵都在於 new,我們可以用函式來模擬 new 做的事情:

1
2
3
4
5
6
7
function newObject(Constructor, arguments) {
var o = new Object(); // 1. 建立新物件
o.__proto__ = Constructor.prototype; // 2. 重新指向原型
Constructor.apply(o, arguments); // 3. 初始化物件
return o; // 4. 回傳新物件
};
let truck = newObject(Car, [6, 2, "柴油"]);

把new做的事情拆解出來:

  1. 建立物件
  2. 把instance的__proto__指向Constructor.prototype
  3. 初始化物件 利用apply將this指派給instance,因為這樣this才可以添加屬性
  4. 回傳新物件

原型鏈 prototype chain

new 關鍵字會把instance的__proto__指向Constructor.prototype,然而在Consturctor.prototype內卻還有一個__proto__

繼續使用上面的範例:

1
console.log(Car.prototype.__proto__);

印出結果會發現:

最後的constructor會指向Object這個constructor

1
console.log(Car.prototype.__proto__ === Object.prototype); // true

更重要的是物件之間的繼承關係,原來是一個接著一個不斷延續的,看起來就像條鎖鏈一樣。

1
2
3
truck.__proto__ === truck.prtotype // Car.prototype
truck.__proto__.__proto__ === truck.prototype.__proto__// Object.prototype
truck.__proto__.__proto__.__proto__ === truck.prototype.prototy.__proto__ // null

原型 prototype 用法

  1. 建構子Book來產出reading_1, reading_2兩個實體
  2. 其中有個共用的方法:setComments
  3. 對共用的方法實作prototype
  4. 在下方就可以直接取用setCommetns的方法
  5. 並且兩個的方法確定是同一個函式

將 setComments 這個共用的方法放到 Book.prototype,就不用每次都幫實體建立一份,提出來放到 Book.prototype 也就是原型裡面即可,讓不同的實體reaind1,2都可以讀取到同樣的函式避免記憶體浪費

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function Book(name, pNum) {
this.name = name; // 書名
this.pNum = pNum; // 頁數
this.comment = null; // 評等
}

Book.prototype.setComments = function(comment) {
this.comment = comment;
}

var reading_1 = new Book('導讀,型別與文法', 257);
var reading_2 = new Book('範疇與閉包 / this 與物件原型', 251);

reading_1.setComments('好書!');
reading_1.comment // "好書!"

reading_2.setComments('超好書!');
reading_2.comment // "超好書!"

reading_1.setComments === reading_2.setComments // true,確認是同一個函式!

請勿修改原生原型

建議設定prototype設定在自己創造的函式上,不要修改原生的(例如:String.prototype),也不要無條件地擴充原生原型,不要使用不要使用原生原型當成變數的初始值,以避免無意間的修改