前言
你是否曾思考為什麼我們能使用 JS 中的一些内置屬性和方法,比如
.length
,
.split()
,
.join()
?我們并沒有顯式地聲明它們,那麼究竟它們從哪裡來的呢?可不要說什麼“那是 JS 中的魔法!”。其實這些都因為一個叫做 原型繼承(prototypal inheritance) 的東西。它太棒啦,你平時也經常會用到,隻不過可能沒有注意!
構造函數
我們經常需要建立很多相同類型的對象。想象一下我們有個網站,上面都是狗狗~
對于每個狗狗來說,我們需要一個對象來表示它!為了不每次都新建立一個對象,就需要寫一個構造函數(稍後再說 ES6 中的類哈~)。有了構造函數,就可以用
new
關鍵字來建立狗狗的 執行個體(instance) 了。
每隻狗都有一個 名字(name),品種(breed),顔色(color) 和一個 吠(bark) 方法。
當我們建立了這個
Dog
構造函數,它并不是我們建立的唯一對象(要知道函數也是對象)。自動地,我們建立了另一個
prototype
對象。預設情況下,這個對象有一個
constructor
屬性,指向的就是我們的
Dog
構造器。
這個在
Dog
構造器上的
prototype
屬性是不可枚舉的,意味着當你嘗試通路對象屬性時,該屬性不會顯示。但是它仍然在那裡!
原型繼承
好吧~那麼為什麼需要有該屬性對象呢?首先,讓我們來建立幾隻狗狗。簡單起見,我們就叫它們
dog1
和
dog2
。
dog1
叫
Daisy
,是一隻可愛的拉布拉多(Labrador)!
dog2
叫
Jack
,是一隻勇敢的傑克羅素犬(Jack Russell)~
讓我們将
dog1
列印到控制台,然後展開它的屬性:
可以看到我們添加的屬性有:
name
,
breed
,
color
和
bark
,但是快看,還有一個
__proto__
屬性!它也是不可枚舉的,是以通常我們在擷取對象屬性的時候也看不到它。讓我們展開看看:
是不是看起來和
Dog.prototype
一樣哈!你猜怎麼着,這個
__proto__
就是對
Dog.prototype
的引用。這就是 原型繼承 的全部内容:構造函數創造的每個執行個體都能夠通路構造函數的原型。
原型繼承的好處
那麼為什麼這很酷?有時候我們擁有每個執行個體共享的屬性。比如
bark
方法:它在每個執行個體中都是相同的,那為什麼每次建立新執行個體都要建立一個
bark
方法,很明顯這樣會浪費記憶體。相反地,我們可以将
bark
方法添加到
Dog.prototype
上去!
這樣每當我們通路執行個體的屬性時,引擎首先檢查該屬性在執行個體上是否定義,如果沒有找到,就會通過
__proto__
屬性,順着原型鍊 繼續查找。
不止是一層
這隻是一個步驟,其實可以包含多個步驟!如果繼續進行下去,你可能會注意到,當我展開
Dog.prototype
的
__proto__
對象時,我沒有包含一個屬性。由于
Dog.prototype
自己也是一個對象,這意味着它實際上是
Object
構造函數的執行個體。這意味着
Dog.prototype
也有一個
__proto__
屬性,并且指向了
Object.prototype
。
比如說
.toString()
這個方法。它是定義在
dog1
上麼?明顯不是。那麼在
Dog.prototype
上有麼?也沒有!其實它是定義在
Dog.prototype.__proto__
上,即
Object.prototype
上。
ES6 中的類
前面我們使用的是構造函數的方式(
function Dog() { ... }
),實際上 ES6 中提供了構造函數和原型的更簡單的文法:類(Classes)
類 隻是 構造函數 的 文法糖。一切都是以相同的方式工作!
我們使用
class
關鍵字來定義類。每個類都有一個
constructor
函數,基本上對應了 ES6 中構造函數的寫法。而我們想要添加到原型
prototype
上的屬性和方法,都可以在類中直接定義。
關于類的另一個好處就是,我們可以輕松地 擴充(extend) 其他的類。
類的繼承
假如我們要添加另一種狗,吉娃娃(Chihuahuas)狗。為了便于了解,我們隻添加一個
name
屬性。但是吉娃娃也可以有自己特殊的叫聲!和普通的叫聲不同,吉娃娃的叫聲可能比較小~
在子類中,我們可以通過
super
關鍵字通路到父類的構造方法。參數當然也參考父類的構造方法傳入。
myPet
可以通路到
Chihuahua.prototype
和
Dog.prototype
(當然也有
Object.prototype
,因為
Dog.prototype
是個對象)。
由于
Chihuahua.prototype
上有一個
smallBark
方法,
Dog.prototype
上有一個
bark
方法,是以我們可以在
myPet
執行個體上同時通路到
smallBark
和
bark
方法。
原型的終點
現在,你可以想象,原型鍊不會永遠持續下去。最終會有一個原型等于
null
的對象:它就是
Object.prototype
。如果我們試圖通路在本地或者原型鍊上都不存在的屬性,最終會傳回
undefined
。
Object.create
盡管上面已經解釋了構造函數和類,其實還有一個為對象添加原型的方式是使用
Object.create
方法。通過這個方法,我們建立了一個新對象,并且指明了這個對象的原型是什麼。
隻需要将一個已經存在的對象傳入
Object.create
方法中。建立出來的對象就是以我們傳入的對象作為原型。看例子:
我們列印一下
me
,可以看到:
我們并沒有為
me
對象添加其他的屬性,但是通路它卻有一個
__proto__
屬性,并且這個屬性指向的是具有
name
和
age
的對象
person
。而
person
這個對象的
__proto__
屬性指向的是
Object.prototype
。
全文就到這裡啦,希望對你學習原型繼承有幫助~
參考連結
- JavaScript Visualized: Prototypal Inheritance
本文首發于公衆号:碼力全開(codingonfire)
本文随意轉載哈,注明原文連結即可