一路上感謝各位讀者們的支持和回饋。
本 30 天系列文目前已經將篇幅重新整理、編纂成冊。
《JavaScript 概念三明治》在天瓏書局上架囉!
喜歡這個系列,想閱讀更詳細原理說明的讀者可以參考:
https://www.tenlong.com.tw/products/9789864347575
前一天我們提到 JS 的原型,以及為什麼會有原型的出現 :為了模擬物件導向的行為。 那麼原型實際上帶來什麼好處?又是透過什麼方式達到繼承的目的?
__proto__
屬性會在物件產生的時候被加到這個物件上,這個 __proto__
就是透過參考的方式,將「被生成物件」與函式的「原型物件」做連結 ( 看到 proto 前後的「_」有沒有把他跟「連結」做聯想,是不是覺得這個變數取的很好? )。這個自動產生 __proto__
參考的行為是 JS 預設的動作,有一點像是這樣:
let user1 = new User()
user1.__proto__ = User.prototype
當然因為這件事情是自動發生的,所以我們不需要手動去做這件事情,在開發上也不建議操作 __proro__
這個變數,請讓他自由,所以整理一下提到的兩個名詞。
當我們想要取用物件中的某個屬性時,JS 會先去物件中尋找該屬性,如果沒有,就會轉而透過__proto__
往原型物件屬性,也就是 prototype
原型物件,去尋找這個屬性。由於原型物件 prototype
本身也是物件,所以我們在這個物件內也可以另外新增屬性,而透過前面的說明我們也可以知道原型物件是在被生成物件之間被共享的,所以我們就可以把一些共用的變數或是方法,放到這個共用的物件之內。
let defaultName = 'Darth Vader'
User.prototype.name = defaultName
let user1 = new User()
let user2 = new User()
user1.name // 'Darth Vader'
user2.name // 'Darth Vader'
這麼做有什麼好處? 把共用函式放在函式建構子裡面的話,每個被生成物件還是會有一樣的函式阿?是這樣沒錯,但是這樣等於是把同樣的數值或函式複製好幾次,生成幾個物件,JS 就會需要幾個記憶體空間;而要同樣的目的,其實只要放在 prototype
原型物件內就可以用較低成本的方式達成。
剛剛說到當 JS 引擎在物件內找不到某個屬性時會透過 __proto__
去往 prototype
原型物件去搜尋這個屬性,如果原型物件裡還是找不到,這個原型物件上也還會有一個 __proto__
,指向他所屬前代類別的原型物件,例如 JS 內 Array 其實也是物件,所以可以說他的前代就是 Object 物件:
Array.prototype.__proto__ === Object.prototype // true
因此 JS 引擎會再透過原型物件裡的 __proto__
屬性往上一個原型物件尋找,直到真的找不到為止 ( 會找到 JS 內 Object 物件的原型物件為止,你可以再透過 __proto__
往上找找,最後會發現他是 null
)。這個行為跟當初我們講到範圍鍊 ( Scope Chain ) 的行為類似,所以也稱為「原型鍊 ( Prototype Chain )」。
最後這個部分就讓我實際的程式碼範例來實作繼承,順便藉此說明原型鍊概念的實用性,在繼承的行為裡,透過被繼承的「後代類別」,所產生出來的物件,一開始就應該要直接具有「前代類別」的屬性跟方法,我們來嘗試看看有沒有辦法透過 JS 達到這個目的。
現在假設:
Human
類別跟 User
類別我們的目標是:透過原型鍊實現 Human 與 User 兩者的繼承關係。
function Human (action, height,race){
this.action = action
this.height = height
this.race = race
}
function User(fisrtname,lastname){
this.fisrtname = fisrtname
this.lastname = lastname
}
Human.prototype.getHumanRace = function(){
return this.race
}
User.prototype.getFullName = function(){
return this.firstname + this.lastname
}
在正式開始之前我們要先思考一下有哪些部分要處理,才能夠讓要繼承的函式建構子與被繼承的函式建構子共享屬性跟方法,主要有兩個方向:
因為透過 new
運算子生成物件的時候,這兩個建構函式上都會各有一個 protorype
物件,一般情況下他們各自為政 ,但是在處理繼承的時候我們必須同時考慮兩者之間的連結。
前面提到物件在找不到屬性時,就會往原型物件找,如果原型物件裡還是找不到,就會再透過原型物件裡的 __proto__
屬性往上一個原型物件尋找,形成原型鍊。原型物件之間要做到繼承就代表了:
透過「後代類別」產生的物件,其上有屬性不管在物件內還是在原型物件上都無法找到時,會轉而往「前代類別」的原型物件尋找
能夠做到這樣子的行為,我們才能說我們透過建立原型屬性的原型鍊,而做到繼承的效果。為了達到這樣子的效果,很顯見的我們必須修改物件上的 __proto__
連結,但是前面也有提過再開發上不建議直接修改__proto__
的參考,因為會破壞物件的預設行為,儘管如此,我們還是可以用比較曖昧的方式來修改這個連結:
User.prototype = Object.create(Human.prototype)
我只用一個之前沒看過的 JS 內建方法 Object.create
修改了繼承物件 User 的 prototype
,Object.create
可以用來創在一個全新的物件,而且他把第一個參數傳入的物件拿來當作這個新物件的 prototype
,之後我們就可以發現 User
的原型物件,被我們修改成一個新的空物件,而這個物件的原型,正是指向 Human
, 透過這樣的方式 ,我們就把兩者之間繼承的原型鍊串起來了。
但是如果你有注意到的話,原本在原型物件上都會有個指回建構函式的prototype.constructor
已經不見了,因此我們需要手動把他加回來,JS 才能夠查找到正確的建構函式。
User.prototype.constructor = User
透過原型物件確實可以達成共享,但如果透過這個方法來共享某些特定屬性,因為屬性的記憶體空間只有一個,這麼一來如果是像「姓名」、「年齡」這種每個人(實體)都會有不同數值的資料,就不適合放在原型物件內,所以我們要想辦法讓我們在「後代」建構函式內可以直接取得「前代」建構函式內容。
簡單來說就是讓前代類別的內容出現在透過後代類別的建構函式所產生的物件上,這裡有一個很經典的辦法,那就是在後代 ( 繼承類別 ) 建構函式裡面執行前代( 被繼承類別 ) 建構函式:
function Human (height,race){
this.height = height
this.race = race
}
function User(fisrtname,lastname,race,height){
this.fisrtname = fisrtname
this.lastname = lastname
Human(height,race) // This is not totally correct
}
這麼一來當 User 透過 new
被呼叫的時候,除了會將 User
內的 this
繫結綁到新生成物件上,還會有另外一個充滿使用 this
繫結來設定物件屬性的 Human
方法被執行,如此一來,前代類別的屬性設置就能夠與後代共用,而前兩行定義的 firstname
與 lastname
,也正好是 User
專屬,Human
不會有的資料屬性,當然我們也可以直接把 Human
內定義的屬性搬到 User 內,不過這樣就會變成是重新定義一整個物件屬性,就失去繼承的意義了:
// dont do this if you want to make an inheritance.
//THIS IS AN ANTI-PATERN
function User(fisrtname,lastname,race,height){
this.fisrtname = fisrtname
this.lastname = lastname
this.height = height
this.race = race
}
但是還沒有完,這邊有一個前面提過很重要的觀念,那就是當我們 執行 Human
方法時,裡面的 this
繫結並非透過 new
被觸發,所以並不是指向剛剛透過 User
函式建構子被生成的新物件,這個時候我們要透過「明確的繫結」來修改 this
的指向,來把 User 內的 this
連結到 Human
函式的 this
上,這樣子我們就達成了所有物件屬性的繼承:
function Human (height,race){
this.height = height
this.race = race
}
function User(fisrtname,lastname,race,height){
this.fisrtname = fisrtname
this.lastname = lastname
Human.call(this,height,race)
}
Human.prototype.getHumanRace = function(){
return this.race
}
User.prototype.getFullName = function(){
return this.firstname + this.lastname
}
User.prototype = new Human()
let user1 = new User('John','Kai','black','179')