一篇文章讓你搞清楚 JavaScript 繼承的本質、prototype
、__proto__
、constructor
都是什麼。javascript
不少小夥伴表示不明白 JavaScript 的繼承,說是原型鏈,看起來又像類,到底是原型仍是類?各類 prototype
、__proto__
、constructor
內部變量更是傻傻搞不清楚。其實,只要明白繼承的本質就很能理解,繼承是爲了代碼複用。複用並不必定得經過類,JS 就採用了一種輕量簡明的原型方案來實現。Java/C++ 等強類型語言中有類和對象的區別,但 JS 只有對象。它的原型也是對象。只要你徹底拋開面向對象的繼承思路來看 JS 的原型繼承,你會發現它輕便但強大。html
prototype
class
__proto__
前面咱們講,繼承的本質是爲了更好地實現代碼複用。再仔細思考,能夠發現,這裏的「代碼」指的必定是「數據+行爲」的複用,也就是把一組數據和數據相關的行爲進行封裝。爲何呢?由於,若是隻是複用行爲,那麼使用函數就足夠了;而若是隻是複用數據,這使用 JavaScript 對象就能夠了:java
const parent = { some: 'data', } const child = { ...parent, uniq: 'data', }
所以,只有數據+行爲(已經相似於一個「對象」的概念)的封裝,纔是繼承技術所必須出現的地方。爲了知足這樣的代碼複用,一個繼承體系的設計須要支持什麼需求呢?git
「支持私有數據」,這個基本全部方案都沒實現,此階段咱們能夠不用糾結;而「增長新成員的能力」,基本全部的方案都能作到,也再也不贅述,主要來看前四點。github
prototype
JavaScript 的繼承有多種實現方式,具體有哪些,推薦讀者可閱讀:[JavaScript 語言精粹][]一書 和 這篇文章。這裏,咱們直接看一版比較優秀的實現:瀏覽器
function Animal(name) { this.name = name this.getName = function() { return this.name } } function Cat(name, age) { Animal.call(this, name) this.age = age || 1 this.meow = function() { return `${this.getName()}eowww~~~~~, I'm ${this.age} year(s) old` } } const cat = new Cat('Lily', 2) console.log(cat.meow()) // 'Lilyeowww~~~~~, I'm 2 year(s) old'
這個方案,具有增添新成員的能力、調用被繼承對象函數的能力等。一個比較重大的缺陷是:對象的全部方法 getName
meow
,都會隨每一個實例生成一份新的拷貝。這顯然不是優秀的設計方案,咱們指望的結果是,繼承自同一對象的子對象,其全部的方法都共享自同一個函數實例。數據結構
怎麼辦呢?想法也很簡單,就是把它們放到同一個地方去,而且還要跟這個「對象」關聯起來。如此一想,用來生成這個「對象」的函數自己就是很好的地方。咱們能夠把它放在函數的任一一個變量上,好比:函數
Animal.functions.getName = function() { return this.name } Cat.functions.meow = function() { return `${this.getName()}eowww~~~~~, I'm ${this.age} year(s) old` }
但這樣調用起來,你就要寫 animal.functions.getName()
,並不方便。不要怕,JavaScript 這門語言自己已經幫你內置了這樣的支持。它內部所用來存儲公共函數的變量,就是你熟知的 prototype
。當你調用對象上的方法時(如 cat.getName()
),它會自動去 Cat.prototype
上去幫你找 getName
函數,而你只須要寫 cat.getName()
便可。兼具了功能的實現和語法的優雅。post
最後寫出來的代碼會是這樣:this
function Animal(name) { this.name = name } Animal.prototype.getName = function() { return this.name } function Cat(name, age) { Animal.call(this, name) this.age = age || 1 } Cat.prototype = Object.create(Animal.prototype, { constructor: Cat }) Cat.prototype.meow = function() { return `${this.getName()}eowww~~~~~, I'm ${this.age} year(s) old` }
請注意,只有函數纔有 prototype
屬性,它是用來作原型繼承的必需品。
class
然鵝,上面這個寫法仍然並不優雅。在何處呢?一個是 prototype
這種暴露語言實現機制的關鍵詞;一個是要命的是,這個函數內部的 this
,依靠的是做爲使用者的你記得使用 new
操做符去調用它才能獲得正確的初始化。可是這裏沒有任何線索告訴你,應該使用 new
去調用這個函數,一旦你忘記了,也不會有任何編譯期和運行期的錯誤信息。這樣的語言特性,與其說是一個「繼承方案」,不如說是一個 bug,一個不該出現的設計失誤。
而這兩個問題,在 ES6 提供的 class
關鍵詞下,已經獲得了很是妥善的解決,儘管它叫一個 class,但本質上實際上是經過 prototype 實現的:
class Animal { constructor(name) { this.name = name } getName() { return this.name } } class Cat extends Animal { constructor(name, age) { super(name) this.age = age || 1 } meow() { return `${this.getName()}eowww~~~~~, I'm ${this.age} year(s) old` } }
new
操做符,編譯器和運行時都會直接報錯。爲何呢,咱們將在[下一篇文章][]講解extends
關鍵字,會使解釋器直接在底下完成基於原型的繼承功能如今,咱們已經看到了一套比較完美的繼承 API,也看到其底下使用 prototype
存儲公共變量的地點和原理。接下來,咱們要解決另一個問題:prototype
有了,實例對象應該如何訪問到它呢?這就關係到 JavaScript 的向上查找機制了。
__proto__
function Animal(name) { this.name = name } Animal.prototype.say = function() { return this.name } const cat = new Animal('kitty') console.log(cat) // Animal { name: 'kitty' } cat.hasOwnProperty('say') // false
看上面 👆 一個最簡單的例子。打出來的 cat
對象自己並無 say
方法。那麼,被實例化的 cat
對象自己,是怎樣向上查找到 Animal.prototype
上的 say
方法的呢?若是你是 JavaScript 引擎的設計者,你會怎樣來實現呢?
我拍腦殼這麼一想,有幾種方案:
Animal
中初始化實例對象 cat
時,順便存取一個指向 Animal.prototype
的引用Animal
中初始化實例對象時,記錄其「類型」(也便是 Animal
)// 方案1 function Animal(name) { this.name = name // 如下代碼由引擎自動加入 this.__prototype__ = Animal.prototype } const cat = new Animal('kitty') cat.say() // -> cat.__prototype__.say() // 方案2 function Animal(name) { this.name = name // 如下代碼由引擎自動加入 this.__type__ = Animal } const cat = new Animal('kitty') cat.say() // -> cat.__type__.prototype.say()
究其實質,其實就是:實例對象須要一個指向其函數的引用(變量),以拿到這個公共原型 prototype
來實現繼承方案的向上查找能力。讀者若是有其餘方案,不妨留言討論。
無獨有偶,這兩種方案,在 JavaScript 中都有實現,只不過變量的命名與咱們的取法有所差別:第一種方案中,實際的變量名叫 __proto__
而不是 __prototype__
;第二種方案中,實際的變量名叫 constructor
,不叫俗氣的 __type__
。實際上,用來實現繼承、作向上查找的這個引用,正是 __proto__
;至於 constructor,則另有他用。不過要注意的是,儘管基本全部瀏覽器都支持 __proto__
,它並非規範的一部分,所以並不推薦在你的業務代碼中直接使用 __proto__
這個變量。
從上圖能夠清楚看到,prototype
是用來存儲類型公共方法的一個對象(正所以每一個類型有它基本的方法),而 __proto__
是用來實現向上查找的一個引用。任何對象都會有 __proto__
。Object.prototype
的 __proto__
是 null,也便是原型鏈的終點。
再加入 constructor 這個東西,它與 prototype
、__proto__
是什麼關係?這個地方,說複雜就很複雜了,讓咱們儘可能把它說簡單一些。開始以前,咱們須要查閱一下語言規範,看一些基本的定義:
__proto__
)][specification: overview]prototype
對象,用以實現原型式繼承,做屬性共享用 這裏說明了什麼呢?說明了構造函數是函數,它比普通函數多一個 prototype
屬性;而函數是對象,對象都有一個原型對象 __proto__
。這個東西有什麼做用呢?
上節咱們深挖了用於繼承的原型鏈,它連接的是原型對象。而對象是經過構造函數生成的,也就是說,普通對象、原型對象、函數對象都將有它們的構造函數,這將爲咱們引出另外一條鏈——
在 JavaScript 中,誰是誰的構造函數,是經過 constructor
來標識的。正常來說,普通對象(如圖中的 cat
和 { name: 'Lin' }
對象)是沒有 constructor
屬性的,它是從原型上繼承而來;而圖中粉紅色的部分便是函數對象(如 Cat
Animal
Object
等),它們的原型對象是 Function.prototype
,這沒毛病。關鍵是,它們是函數對象,對象就有構造函數,那麼函數的構造函數是啥呢?是 Function
。那麼問題又來了,Function
也是函數,它的構造函數是誰呢?是它本身:Function.constructor === Function
。由此,Function
便是構造函數鏈的終結。
上面咱們提到,constructor
也能夠用來實現原型鏈的向上查找,而後它卻別有他用。有個啥用呢?通常認爲,它是用以支撐 instanceof
關鍵字實現的數據結構。
好了,是時候進入最燒腦的部分了。前面咱們講了兩條鏈:
Object.prototype
,終結於 null
,沒有循環Function
把這兩條鏈結合到一塊兒,你就會看到一條雙螺旋 DNA這幾張你常常看到卻又看不懂的圖:
圖都是引用自其它文章,點擊圖片可跳轉到原文。其中,第一篇文章 [一張圖理解 JS 的原型][] 是我見過解析得最詳細的,本文的不少靈感也來自這篇文章。
理解了上面兩條鏈之後,這兩個全圖實際上就不難理解了。分享一下,怎麼來讀懂這個圖:
constructor
都會指向它們的構造函數;而構造函數也是對象,它們最終會一級一級上溯到 Function
這個構造函數。Function
的構造函數是它本身,也即此鏈的終結;Function
的 prototype
是 Function.prototype
,它是個普通的原型對象;__proto__
都會指向其構造函數的原型對象 [Class].prototype
;而全部原型對象,包括構造函數鏈的終點 Function.prototype
,都會最終上溯到 Object.prototype
,終結於 null。也便是說,構造函數鏈的終點 Function
,其原型又融入到了原型鏈中:Function.prototype -> Object.prototype -> null
,最終抵達原型鏈的終點 null
。至此這兩條契合到了一塊兒。
總結下來,能夠歸納成這幾句話:
Function
,包括 Function
本身Object.prototype
,包括 Function.prototype
,終止於 null
這裏還有最後一個所謂「雞生蛋仍是蛋生🐔」的問題:是先有 Object.prorotype
,仍是先有 Function
?若是先有前者,那麼此時 Function
還不在,這個對象又是由誰建立呢?若是先有後者,那麼 Function
也是個對象,它的原型 Function.prototype.__proto__
從哪去繼承呢?這個問題,看似無解。但從 這篇文章:從__proto__和prototype來深刻理解JS對象和原型鏈 中,咱們發現了一個合理的解釋,那就是:
Object.prototype
是個神之對象。它不禁Function
這個函數構造產生。
證據以下:
Object.prototype instanceof Object // false Object.prototype instanceof Function // false Object.prototype.__proto__ === Function.prototype // false
JS 對象世界的構造次序應該是:Object.prototype
-> Function.prototype
-> Function
-> Object
-> ...
講到這裏,我想關於 JavaScript 繼承中的一些基本問題能夠解釋清楚了:
JavaScript 繼承是類繼承仍是原型繼承?不是使用了 new 關鍵字麼,應該跟類有關係吧?
是徹底的原型繼承。儘管用了 new
關鍵字,但其實只是個語法糖,跟類沒有關係。JavaScript 沒有類。它與類繼承徹底不一樣,只是長得像。比如雷鋒和雷峯塔的關係。
prototype
是什麼東西?用來幹啥?
prototype
是個對象,只有函數上有。它是用來存儲對象的屬性(數據和方法)的地方,是實現 JavaScript 原型繼承的基礎。
__proto__
是什麼東西?用來幹啥?
__proto__
是個指向 prototype
的引用。用以輔助原型繼承中向上查找的實現。雖然它獲得了全部瀏覽器的支持,但並非規範所推薦的作法。嚴謹地說,它是一個指向 [[Prototype]]
的引用。
constructor
是什麼東西?用來幹啥?
是對象上一個指向構造函數的引用。用來輔助 instanceof
等關鍵字的實現。
🐔生蛋仍是蛋生🐔?
神生雞,雞生蛋。
__proto__
][]