建構式的原型:Constrctor.prototype
上個單元中我們認識了原型和原型鏈的概念,在這個單元中,我們則要進一步把這些概念應用於 constructor function 裡。
我們會接續之前寫的 AlphaPhone
的建構式:
// Constructor Function
function SmartPhone (name, price, features){
// property
this.name = name
this.price = price
this.features = features
// method
this.showPhoneInfo = function() {
console.log(`The price of ${this.name} is $${this.price}, which has the newest features such as ${this.features.join(', ')}.`)
}
}
如果把方法寫在建構式函式中⋯⋯
之前我們提到「建議不要把方法寫在 constructor 裡」,到底這樣會發生什麼事呢?如果我們直接透過 SmartPhone
來產生物件:
let alphaPhoneX = new SmartPhone('alphaPhoneX', 14999, ['long battery life', 'AI camera'])
let alphaPhoneY = new SmartPhone('alphaPhoneY', 18900, ['water proof', 'high screen resolution'])
let alphaPhoneZ = new SmartPhone('alphaPhoneZ', 23900, ['IP47', 'high screen resolution', 'full display'])
你會發現在每個物件裡面都包含了一個一模一樣的 showPhoneInfo
方法:
// alphaPhoneX
SmartPhone {
name: "alphaPhoneX",
price: 14999,
features: ["long battery life", "AI camera"],
showPhoneInfo: function () { ... }
}
// alphaPhoneY
SmartPhone {
name: "alphaPhoneY",
price: 18900,
features: ['water proof', 'high screen resolution'],
showPhoneInfo: function () { ... }
}
// alphaPhoneZ
SmartPhone {
name: "alphaPhoneZ",
price: 23900,
features: ['IP47', 'high screen resolution', 'full display'],
showPhoneInfo: function () { ... }
}
這些物件存放於電腦的記憶體中,而之前我們提過物件是 share by reference 的,因此別忘了每個物件中的 showPhoneInfo
方法也會另外存放在記憶體裡,再用參照位址指回來。
所以這裡總共有三個 SmartPhone 實例物件、和三個物件中的 method,各自佔有獨立的記憶體空間。
但顯然我們不需要 3 個 method。那麼在學過原型和原型鏈的概念後,我們可以怎麼樣做來避免這個問題呢?
將共用的方法放在原型中:Constructor.prototype
之前提到,當我們呼叫某個物件的方法時,JavaScript 會透過原型鏈尋找該方法是否存在。也就是說,只需要把方法放進 constructor 的原型裡,每個物件實例就能呼叫到這個共享的方法。
定義 SmartPhone.prototype
讓我們把建構式裡的 showPhoneInfo()
移到 SmartPhone.prototype
中:
function SmartPhone (name, price, features){
this.name = name
this.price = price
this.features = features
}
SmartPhone.prototype.showPhoneInfo = function() {
console.log(`The price of ${this.name} is $${this.price}, which has the newest features such as ${this.features.join(', ')}.`)
}
這麼做的話,會發生什麼事呢?
再次調用 showPhoneInfo
讓我們再次透過 SmartPhone
來產生兩個物件實例:
let alphaPhoneX = new SmartPhone('alphaPhoneX', 14999, ['long battery life', 'AI camera'])
let alphaPhoneY = new SmartPhone('alphaPhoneY', 18900, ['water proof', 'high screen resolution'])
接著我們分別呼叫 showPhoneInfo
:
alphaPhoneX.showPhoneInfo()
alphaPhoneY.showPhoneInfo()
它們一樣能成功呼叫 showPhoneInfo()
,但是當我們用 console.log()
印出 alphaPhoneX
時,你不會再看到 showPhoneInfo()
:
觀察 proto
showPhoneInfo
到哪裡去了呢?如果把 __proto__
打開來看,你會發現 showPhoneInfo
跑到了原型裡:
這個 .__proto__
會指向 SmartPhone.prototype
,使用同一個建構式產生的物件實例 (instance) 共享同一個原型,你可以用以下方式確認:
alphaPhoneX.__proto__ === alphaPhoneY.__proto__ // true
使用同一個建構式產生的物件實例 (instance) 共享同一個原型。
(注意這裡的名稱很混淆,__proto__
是指向 SmartPhone.prototype
,而 .prototype
是 constructor 定義原型的方法,在名稱上都稱作原型,不只是你,大家在溝通時常常會搞混,如果不清楚,隨時在 console 上實驗一下)
使用 Object.getPrototypeOf(alphaPhoneX)
來查看也會得到一樣的結果。
雖然 showPhoneInfo
方法沒有直接被放在物件裡,但透過原型鏈的運作,我們仍然能成功呼叫到它:
alphaPhoneX.constructor
你會發現在 alphaPhoneX.__proto__
裡面還有一個 __proto__
,讓我們再打開來看看,你會發現一個名為 constructor 的屬性:
這個 constructor
指向 SmartPhone
建構式** (讓實例知道怎麼認祖歸宗) :
alphaPhoneX.constructor === SmartPhone // true
或者,可以透過 constructor.name
這樣的方式來確認建構式的名稱:
console.log(alphaPhoneZ.constructor.name) // "SmartPhone"
動態修改 Prototype
現在你在 SmartPhone.prototype
裡定義好共用方法了,如果哪天想要修改原型裡的方法,你不需要回頭找到原始的程式碼再修改,只要再定義一次SmartPhone.prototype.showPhoneInfo
就可以了。
例如以下這一整段程式碼中,我們呼叫了兩次 alphaPhoneX.showPhoneInfo()
:
// Constructor Function
function SmartPhone (name, price, features){
this.name = name
this.price = price
this.features = features
}
// Put methods in protoype
SmartPhone.prototype.showPhoneInfo = function() {
console.log(`The price of ${this.name} is $${this.price}, which has the newest features such as ${this.features.join(', ')}.`)
}
// create alphaPhoneX instance
let alphaPhoneX = new SmartPhone('alphaPhoneX', 14999, ['long battery life', 'AI camera'])
alphaPhoneX.showPhoneInfo()
// Modify showPhoneInfo in prototype
SmartPhone.prototype.showPhoneInfo = function() {
console.log(`The phone '${this.name}' with the newest features such as ${this.features.join(', ')}, only cost $${this.price}`)
}
// same instance but with different result
alphaPhoneX.showPhoneInfo()
你會發現第二次呼叫內容,輸出的訊息已經被改變:
雖然我們在修改 SmartPhone.prototype.showPhoneInfo
這個方法之前就用 new
建立了 alphaPhoneX
實例,但是呼叫 alphaPhoneX.showPhoneInfo()
的時候,都會動態參照 SmartPhoneInfo.protoype
,因此你一定會得到最新的 showPhoneInfo
的結果。
Recap
相信看到這裡,這些新術語、新語法讓你有眼花繚亂的感受。其實沒那麼複雜,簡單來說就是:
- Constructor 裡放共享的屬性
- 共享的方法放進 Constructor.prototype
- 用 instance.constructor 可以找到原本的建構式