JS 繼承的從入門到理解
作者 | 張文慧
開場白
大三下學期結束時候,一個人跑到帝都來參加各廠的面試,免不了的面試過程中經常被問到的問題就是JS中如何實現繼承,當時的自己也是背熟了實現繼承的各種方法,回過頭來想想卻不知道__proto__是什麼,prototype是什麼,以及各種繼承方法的優點和缺點,想必有好多剛入坑的小夥伴有著跟我一樣的體驗,這篇文章將從基礎概念出發,進一步說明js繼承,以及各種繼承方法的優缺點,希望對看這篇文章的你有所幫助,如果你是見多識廣的大佬,既然看到這裡了,不妨繼續看下去,指點一二,讓新入坑的小夥伴更好的成長。(如果你都看到這了,透露一下文末有彩蛋嗷!)下面,我們進入正題:
設計思想
如果你沒看過,也會聽別人說JavaScript的繼承不同於Java和c++,js中沒有“類”和“例項”的區分,而是靠一種原型鏈的一級一級的指向來實現繼承。那麼當時的創造JavaScript這種的語言的人為什麼要這樣實現js獨有的繼承,大家可以閱讀阮一峰老師的Javascript繼承機制的設計思想(http://www.ruanyifeng.com/blog/2011/06/designing_ideas_of_inheritance_mechanism_in_javascript.html),就像講故事一樣,從古代至現代說明了js繼承這種設計模式的緣由。
prototype物件
瞭解了js繼承的設計思想後,我們需要學習原型鏈上的第一個屬性prototype,這個屬性是一個指標,指向的是原型物件的記憶體堆。從阮一峰老師的文章中,我們可以知道prototype是為了解決建構函式的屬性和方法不能共享的問題而提出的,下面我們先實現一個簡單的繼承:
此時,例項1 和例項2 都有自己的data屬性、state屬性、isPlay方法,造成了資源的浪費,既然兩個例項都需要呼叫isPlay方法,便可以將isPlay方法掛載到建構函式的prototype物件上,例項便有了本地屬性方法和引用屬性方法,如下:
我們將isPlay方法掛載到prototype物件上,同時增加isDoing屬性,既然是共享的屬性和方法,那麼修改prototype物件的屬性和方法,例項的值都會被修改,如下:
問題來了,為什麼例項會取到prototype物件上的屬性和方法,別急,沒多久就會結合其他問題綜合解答。
同時,你可能會問,如果修改例項1的isDoing屬性的原型,例項2的isDoing會不會受到影響?
問題又來了,可以看到修改例項1的isDoing屬性,例項2的例項並未受到影響。這是為什麼呢?
那如果修改例項1的isDoing屬性的原型屬性,例項2的isDoing會不會受到影響?如下:
問題又又來了,為什麼修改例項1的__proto__屬性上的isDoing的值就會影響到建構函式的原型物件的屬性值?
我們先整理一下,未解決的三個問題:
-
為什麼例項會取到prototype物件上的屬性和方法?
-
為什麼修改例項1的isDoing屬性,例項2的例項沒有受到影響?
-
為什麼修改例項1的__proto__屬性上的isDoing的值就會影響到建構函式的原型物件的屬性值?
這時候不得不背後真正的操作者搬出來了,就是new操作符,同樣是面試最火爆的問題之一,new操作符幹了什麼?相信有人也是跟我一樣,已經背的滾瓜爛熟了,以 Var instance1 = new constructorFn();為例,就是下面三行程式碼:
第一行宣告一個空物件,因為例項本身就是一個物件。 第二行將例項本身的__proto__屬性指向建構函式的原型,obj新增了建構函式prototype物件上掛載的屬性和方法。 第三行將建構函式的this指向替換成obj,再執行建構函式,obj新增了建構函式本地的屬性和方法。
理解了上面三行程式碼的含義,那麼三個問題也就迎刃而解了。
問題1:例項在新建的時候,本身的__ptoto__指向了建構函式的原型。
問題2:例項1和例項2 在新建後,有了各自的this,修改例項1的isDoing屬性,只是修改了當前物件的isDoing的屬性值,並沒有影響到建構函式。
問題3:修改例項1的__proto__,即修改了建構函式的原型物件的共享屬性
到此處,涉及到的內容大家可以再回頭捋一遍,理解了就會覺得醍醐灌頂。
__proto__
同時,你可能又會問,__proto__是什麼?
簡單來說,__proto__是物件的一個隱性屬性,同時也是一個指標,可以設定例項的原型。 例項的__proto__指向建構函式的原型物件。
需要注意的是,
每個物件都有內建的__proto__屬性,函式物件才會有prototype屬性。
用chrome和FF都可以訪問到物件的__proto__屬性,IE不可以。
我們繼續用上面的例子來說明:
建構函式的原型物件也是物件,那麼constructor.prototype.__proto__指向誰呢?
定義中說物件的__proto__指向的是建構函式的原型物件,下面我們驗證一下constructor.prototype.__proto__的指向:
用圖形表示的話,如下:
可以看出,constructor.prototype.__proto__的指向是Object的原型物件。
那麼,Object.prototype.__proto__的指向呢?
用圖形表示的話,如下:
可以發現,Object.prototype.__proto__ === null; 這樣也就形成了原型鏈。通過將例項的原型指向建構函式的原型物件的方式,連通了例項-建構函式-建構函式的原型,原型鏈的特點就是逐層查詢,從例項開始查詢一層一層,找到就返回,沒有就繼續往上找,直到所有物件的原型Object.prototype。
繼承的方法
瞭解了上面的基礎概念,就要將學到的用在實際當中,到底要怎麼實現繼承呢?實現的方式有哪些?下面主要說明實現繼承最常用的三用方式,可以滿足基本的開發需求,想要更深入的瞭解,可以參考阮一峰老師的網路部落格(http://www.ruanyifeng.com/blog/)。
原型鏈繼承
實現原理:將父類的例項作為子類的原型
注:
-
這種繼承方式需要將子類的建構函式指回本身,因為從父類繼承時同時也繼承了父類的建構函式。
-
簡單的使用Cat.prototype = Animal.prototype將會導致兩個物件共享相同的原型,一個改變另一個也會改變。
-
不要使用Cat.prototype = Animal,因為不會執行Animal的原型,而是指向函式Animal。因此原型鏈將會回溯到Function.prototype,而不是Animal.prototype,因此canRun將不會在Cat的原型鏈上。
使用call、apply方法實現
實現原理:改變函式的this指向
注:
-
該方法將子類Cat的this指向父類Animal,但是並沒有拿到父類原型物件上的屬性和方法
使用混合方法實現
實現原理:原型鏈可以繼承原型物件的屬性和方法,建構函式可以繼承例項的屬性且可以給父類傳參
每一種繼承方式都有自己的優點和不足,讀者可以根據實際情況選擇相應的方法。為了在實際開發中更方便的使用繼承,可以封裝一個繼承的方法,如下:
結合一開始的例子,可以這樣實現繼承的關係:
javaScript的繼承遠不止這些,,只希望可以讓新學js的小夥伴不那麼盲目的去刻意記一些東西,當然學習最好的辦法還是要多寫,最簡單的就是直接開啟瀏覽器的控制檯,去驗證自己各種奇奇怪怪的想法,動起來吧~
|-------赤裸裸的分割線-------|
彩蛋來啦:本週我們的轉轉app 5.6版本就要正式發版啦,新版本新增了小視訊功能呢,大家可以通過小視訊分享自己各種購物經驗,也可以發揮自己的腦洞,展示自己的才華,快來給我們的開發小哥哥打call吧~