今天是鐵人賽的第十五天,旅程進行到了一半,而本系列文 JavaScript 的部分也即將告一段落。經過了連續三天型別相關的內容,今天也將接續下去,跟大家聊聊許多開發者時常搞不清楚的「物件原型」及「原型鏈」。
本系列文已經重新編校彙整編輯成冊,並正式出版囉!
《前端三十:從 HTML 到瀏覽器渲染的前端開發者必備心法》好評販售中!
喜歡我文章內容的讀者們,歡迎您 前往購買 支持!
相信大家多少都有用過 map
將陣列內容依序處理的經驗吧?例如:
let arr = [0, 1, 2]
let doubleArr = arr.map(c => c * 2)
console.log(doubleArr) // 0, 2, 4
不知道你有沒有曾好奇過,像這邊的變數 arr
,本身並沒有設定 map
屬性,那為什麼可以使用 map
這個函式呢?
我們將它印出來看看:
console.log(arr)
// 0: 0
// 1: 1
// 2: 2
// length: 3
// __proto__: Array(0)
出現了名為 __proto__
的物件,我們可以再將其展開,便會看到所有 Array 物件可以使用的函式;當然,我們也可以在其中找到 map
函式,而這也正是我們範例中呼叫 arr.map
的那個函式:
console.log(arr.map === arr.__proto__.map) // true
這邊出現的 __proto__
物件,也就是所謂的 原型物件(Prototype) 。
不同於 Java、C# 等 基於類別(Class) 的物件導向程式語言,藉由透過定義 Class、建立實例(instance)、指定繼承等方式來傳遞屬性及方法;Javascript 則是個 基於原型(Prototype)的物件導向程式語言 ,透過預先建立出的原型物件,每當新物件建立時,便指定物件的原型要參照到哪個原型物件。
而當開發者呼叫物件的屬性或方法時,若物件本身沒有這項屬性/方法,JavaScript 會自動尋找它原型中的方法,這也就是為什麼我們可以直接呼叫 arr.map
而不會出錯的原因了。
眼尖的讀者可能已經發現了,在前述的例子中,__proto__
物件中仍然有 __proto__
屬性:
console.log(arr.__proto__) // Array 的 Prototype
console.log(arr.__proto__.__proto__) // Object 的 Prototype
console.log(arr.__proto__.__proto__.__proto__) // null
前述的機制中,每當物件建立,都會綁定原型,既然物件都有原型,物件原型本身也是物件,自然也就不例外;由這邊的例子中我們可以看出:
arr
是陣列實例,原型是 Arrayarr.__proto__
是陣列的原型,原型是 Objectarr.__proto__.__proto__
是物件的原型,原型是 null
arr.__proto__.__proto__.__proto__
是 null,沒有任何屬性由於每一個物件都有原型,這樣一個牽一個、層層相依的從屬關係,我們會把它們叫做 原型鏈(Prototype Chain) ;透過前述的機制,讓物件得以使用原型中的屬性 & 方法,並藉由原型鏈一層一層的依序繼承,讓物件能擁有所有原型鏈上原型的功能,這就是 JavaScript 每個物件背後的運作機制。
小補充,在 JavaScript 中,幾乎每個原型鏈的末端都會在 Object,並最後指向到
null
。
說了那麼多,好像該來點 Code 了,那麼就來練習一下原型的建立、設定及修改吧~
首先先來建立一個新的物件建構子:
function Person(name) {
this.name = name
}
Person.prototype.hello = function () {
console.log(`Hello ${this.name}.`)
}
let gary = new Person('Gary')
gary.hello() // Hello Gary.
Object.getPrototypeOf(gary) // {hello: ƒ, constructor: ƒ}
如上面的範例,撰寫了一個簡單的物件建構子 Person()
,並在建構子中設定物件屬性。物件方法的部分,由於方法不需要讓每個物件都獨自擁有一份,以避免造成冗餘的記憶體消耗,應該要如同先前 Array.prototype.map
的範例,將物件的方法設定給原型物件(Person.prototype
),讓此建構子出來的物件都可以共用這些方法。最後建立一個新的 Person
物件,並藉由 getPrototypeOf(obj)
,取得新產生物件的原型。
Q:為什麼不直接用
__proto__
取得原型物件?
A:因為__proto__
雖然被幾乎所有的瀏覽器支援,但它仍是非標準屬性;透過getPrototypeOf
取得物件的原型是比較正確的方法。
小提醒:
Person.prototype
不是Person
的原型,而是建構子函數執行後,所建立的新物件的原型;千萬不要把「建構子的prototype
屬性」與「物件的原型」搞混囉!
接著我們再建立新的物件原型,並繼承 Person
:
function Engineer(name, skill) {
Person.call(this, name)
this.skill = skill
}
Engineer.prototype = Object.create(Person.prototype)
Engineer.prototype.constructor = Engineer
let alice = new Engineer('Alice', 'JavaScript')
alice.hello() // Hello Alice.
console.log(alice.skill) // JavaScript
Object.getPrototypeOf(alice) // Person {constructor: ƒ}
這邊我們建立了新的物件 Engineer
的原型,並透過 Engineer.prototype
的指定,讓它的原型繼承自 Person.prototype
,最後再重新設定 Engineer.prototype.constructor
,讓建構子重新指回自己;這樣我們就完成最基本的原型繼承囉。
Q:為什麼需要重新設定
constructor
?
A:這邊透過Object.create
複製了Person.prototype
全部的屬性,連同constructor
屬性都會被蓋掉,如果constructor
屬性錯誤,在做instanceof
判斷時會造成錯誤的結果;因此這邊設定繼承時需要再將constructor
重新指定回建構子本身。
原型的引用、繼承,是直接參照到原型物件上,並非是在每個物件都複製一份原型;因此我們可以利用這個特性,在原型上增加自訂的屬性、方法,讓所有該類型的物件都能得到新方法;許多針對舊版瀏覽器的 Polyfill 便是這樣達成的。
例如有寫過 Vue 的開發者,可能多少都有做過類似的操作,把共用性高的屬性方法放到 Vue.prototype
中:
Object.defineProperty(Vue.prototype, '$date', { value: dateTimeFormat })
// 之後就可以這樣使用
vm.$date(dateObj)
這樣很方便沒錯,但也要特別提醒開發者們,當我們在做原型修改的時候,需要特別小心。接續剛剛的例子,我們嘗試對 Person
原型中的方法做個修改:
Person.prototype.hello = function () {
console.log(`Bye ${this.name}.`)
}
gary.hello() // Bye Gary.
alice.hello() // Bye Alice.
如範例結果所見,當我們對物件原型做修改時,所有原型鏈上有此原型的物件,通通都會被連帶影響,無論物件的建立時間是在修改前或修改後。
建議大家,除非是 Polyfill,否則應該要極力避免對原生物件的原型進行修改,防止可能造成的意外結果。
看完前面一大段,看著一大堆繁雜的設定,是不是覺得心很累、頭很痛?別擔心,從 ES6 開始就有了 Class
語法可以使用,整體開發者體驗會好上不少。讓我們把前面的範例透過 Class
重構一下:
class Person {
constructor (name){
this.name = name
}
// 方法會自動放到 Person.prototype
hello() {
console.log(`Hello ${this.name}.`)
}
}
class Engineer extends Person {
constructor (name, skill){
super(name) // 呼叫 Person 的建構子
this.skill = skill
}
}
let alice = new Engineer('Alice', 'JavaScript')
alice.hello() // Hello Alice.
Object.getPrototypeOf(alice) // Person {constructor: ƒ}
很方便吧,一樣功能的程式,可讀性卻提高了不少,過程中繁瑣的設定也都能交給語法自動幫你完成;當然,方便的語法背後,底層仍然是仰賴物件原型及原型鏈。
以上就是 JavaScript 中關於物件原型的說明介紹,希望藉由本文的內容及範例,能幫助讀者您理解物件原型,在這個什麼都是物件的語言中,充分理解、掌握物件原型,也是成為更好的工程師路上,必須突破的關卡之一。
系列文中關於 JavaScript 的部分就到這邊啦,接下來的前端三十之旅會走向哪裡呢?就請大家繼續追蹤本系列文囉!
筆者
Gary
半路出家網站工程師;半生熟的前端加上一點點的後端。
喜歡音樂,喜歡學習、分享,也喜歡當個遊戲宅。相信一切安排都是最好的路。
Q:為什麼需要重新設定 constructor?
在做 instanceof 判斷時會造成錯誤的結果?這部份不是很清楚,能否請大大再指導一下,謝謝。
我也蠻好奇的就測試一下
不過在chrome, (t instanceof Teacher)還是會成功耶
"Person.prototype 不是 Person 的原型,而是建構子函數執行後,所建立的新物件的原型;千萬不要把「建構子的 prototype 屬性」與「物件的原型」搞混" 這段聽不懂QQ
假設Person1 = new Person('John'), 新物件Person1的原型是Person1.proto=== Person.prototype, 所以Person.prototype是Person1的原型,而不是Person的