原型鏈(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 | function Car(wheel, door, fuel) { |
你會發現 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時會發生:
- instance會初始化,並可以透過建構函式新增屬性
- instance的
__proto__
跟建構函式的prototype是一樣的
這邊透過函式使用this設定屬性非常奇怪,this.屬性這樣的方式做添加,不太合理,因為照理來說這樣會加到全域的屬性上面,所以關鍵出在new
其實一切的關鍵都在於 new,我們可以用函式來模擬 new 做的事情:
1 | function newObject(Constructor, arguments) { |
把new做的事情拆解出來:
- 建立物件
- 把instance的
__proto__
指向Constructor.prototype - 初始化物件 利用apply將this指派給instance,因為這樣this才可以添加屬性
- 回傳新物件
原型鏈 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 | truck.__proto__ === truck.prtotype // Car.prototype |
原型 prototype 用法
- 建構子Book來產出reading_1, reading_2兩個實體
- 其中有個共用的方法:setComments
- 對共用的方法實作prototype
- 在下方就可以直接取用setCommetns的方法
- 並且兩個的方法確定是同一個函式
將 setComments 這個共用的方法放到 Book.prototype,就不用每次都幫實體建立一份,提出來放到 Book.prototype 也就是原型裡面即可,讓不同的實體reaind1,2都可以讀取到同樣的函式避免記憶體浪費
1 | function Book(name, pNum) { |
請勿修改原生原型
建議設定prototype設定在自己創造的函式上,不要修改原生的(例如:String.prototype),也不要無條件地擴充原生原型,不要使用不要使用原生原型當成變數的初始值,以避免無意間的修改