首先,最重要的一句話:在 js 的世界裏,不存在類繼承,只有對象繼承。java
在 js 誕生之初是沒有類這個概念的,只有用來建立對象的構造函數,而函數自己只是一種特殊的對象。即使後來出現了 class,也沒有改變本質。js 的 class 和 c++ / java 裏面的 class 有本質區別。js 的 class 幾乎只是構造函數的語法糖,下面兩種寫法時等價的:c++
class Person { constructor(name) { this.name = name } getName() { return this.name } }
function Person(name) { this.name = name } Person.prototype.getName = function() { return this.name }
實際上第二種寫法更加本質。定義一個類,其實沒有類,實際上是定義了兩個對象:一個構造函數 Person 加一個原型對象 Person.prototype。當用 new Person('張三') 創造出「張三」這個對象時,「張三」的原型自動指向 Person.prototype,這樣它就擁有了 getName()。編程
prototype vs [[prototype]],Object vs Function數組
原型鏈之因此很是讓人迷惑,就是由於有這兩對東西。瀏覽器
第一對,prototype 不是 [[prototype]],它們是兩個不一樣的指針。[[prototype]] 是對象中真正指向原型的指針,通常是不可見的,須要經過 Object.getPrototypeOf() 獲取,但在一些瀏覽器中能夠用 __proto__ 取到。prototype 就是在定義構造函數的時候用到的那個 prototype,它是構造函數的一個屬性,指向一個對象,當 new 出來實例時,該對象會成爲實例的原型,也就是說,實例的 [[prototype]] 會指向構造函數的 prototype。在上面的例子中,「張三」 的 [[prototype]] = Person.prototype。app
第二對,Object 和 Function 分別是對象和函數的最原始的構造函數,可是 Object instanceof Function 的結果是 true,Function instanceof Object 也是 true。好了,究竟是先有雞仍是先有蛋呢?誰纔是最終的那個造物主呢?函數
這兩對困惑實際上是一體兩面,背後是同一個東西,也就是下面這張圖:this
這個圖中間有一條虛線,劃分爲上下兩個部分。上面部分是在 js 代碼執行以前,就由系統初始化好,存在於全局當中的。下面部分是以後,用戶寫的 js 代碼建立的對象。spa
上面部分有兩個很是特殊的對象,暫且將其稱做 AoO (ancestor of object,對象的祖先)和 AoF(ancestor of function, 函數的祖先)。這兩個對象就像亞當和夏娃,在一切 js 代碼執行以前就被創造出來,承擔全部對象祖先的角色。prototype
其中,AoO 裏面定義了一些很是通用的,全部對象都會繼承到的方法,典型如 toString()。AoO 的 [[prototype]] 指向 null。
AoF 裏面定義了全部函數會繼承到的方法,典型如 apply()。AoF 的 [[prototype]] 指向 AoO。
而後是 Function,它的特別之處在於它能夠建立函數,並且它的 prototype 和 [[prototype]] 都指向 AoF。
再來是 Object,Object 負責建立對象,因此它的 prototype 指向 AoO,這樣全部它 new 出來的實例纔會繼承 AoO。可是有意思的,Object 的 [[prototype]] 指向 AoF,這使得 Object 看起來好像是由 Function new 出來的,但實際上不是。這是系統刻意這樣安排,由於,Object 也是一個函數,理論上,Object 應該是 Function 的一個實例。所以,Object instanceof Function 爲 true。而 Function instanceof Object 也爲 true,由於 AoO 也在 Function 的原型鏈上,只不過中間隔了一層 AoF。
因此,Object 和 Function 互爲彼此的實例,並非由於它們互相建立出了對方,而是系統刻意這樣安排它們的原型鏈,從而達到這樣一種效果。
以後就到了虛線下面的部分。當運行下面的代碼:
class Person { constructor(name) { this.name = name } getName() { return this.name } }
時,就建立了 Person 和 Person.prototype。即便不定義 Person.prototype,Person.prototype 也會默認存在。以後再 new,就出現了「張三」、「李四」等等。
接着,再定義一個子類:
class Woman extends Person {
constructor(name) {
super(name)
this.gender = 'female'
}
getGender() {
return this.gender
}
}
就創造出了 Woman 構造函數和 Woman.prototype,而且 Woman 繼承自 Person,Woman.prototype 繼承自 Person.prototype。而後 Woman 的實例繼承自 Woman.prototype。至此,造成了兩條互相平行的原型鏈:
1,王五 -> Woman.prototype -> Person.prototype -> AoO
2,Woman -> Person -> AoF
最終 AoF -> AoO 進行匯合,萬物歸宗,所有都繼承自 AoO。
基因造人 vs 模板造人
c++ / java 的 class 和對象的關係至關於基因和人的關係。class 是基因,由基因產生出來的人,一生都擺脫不了這個身份。張三是黃種人,那他永遠都是黃種人。黃種人是張三不可分割的我的特徵,寫在臉上,很是明顯。
而 js 造人,是像女媧造人同樣,參照着某一個模板把人捏出來。構造函數就是這個模板。張三出生在中國,可是中國人並無明顯地寫在他臉上,他一輩子中可能移民幾回,前後變成了美國人、日本人,最後又變回中國人,都有可能。這是由於,能夠經過 Object.setPrototypeOf() 來修改對象的原型,從而致使張三的身份是可變的。
可變類型還能夠產生另外一個效果:能夠先創造出對象,再來設計對象的類型。先用一個空的構造函數創造出許多對象,而後根據須要,往構造函數的 prototype 中添加方法。
鴨式辨型
既然 js 中的類型這麼變化無常,那麼在用 js 編程的時候就要屏棄傳統的類型思惟。鴨式辨型的意思是:「像鴨子同樣走路而且嘎嘎叫的動物就是鴨子」。雖然它可能其實是一隻奇怪的雞。這很有一種英雄莫問出處,只看長相的意味。鴨式辨型在 ts 中獲得了進一步的支持和發展。ts 的 interface 也很特別,咋看起來它好像和傳統語言的 interface 是同樣的,但其實其背後的設計思路徹底不一樣。ts 的 interface 是一種對結構的描述,編譯器根據這個描述來作類型檢查。因而,它並不要求對象顯示地實現 interface,只要能經過檢查就行。並且,本着「結構描述」這個定位,ts 的 interface 作得比傳統的 interface 更強大,好比它還能夠描述傳入的對象必須是能夠經過數組下標的方式去訪問的,或者描述傳入的對象必須是一個 class 的 constructor 等等。
純對象的世界
模板造人和鴨式辨型,其實折射出 js 底層是一個沒有類的世界。在一個沒有類的世界裏,一切都是自由的。在提供了巨大的靈活性的同時,也致使了不可控的問題。靈活性是一把雙刃劍,高手拿到這把劍削鐵如泥,而普通人拿到這把劍則可能傷到本身。並且太靈活也給工程化增長障礙,這也是 ts 出現的緣由之一。可是,儘管有這些問題,js 依然是很是有特點的語言,畢竟類的存在乎義就是建立實例,而絕大多數時候,人們其實只須要建立一個實例,卻不得不爲了這個實例去定義類。定義了類,就必須管理這些類的繼承關係,同時類型檢查也是基於類。這樣就變成了面向類型編程,而真正重要的實際上是對象。運行時存在的是對象,完成工做的是對象。人們面向類型編程,其實白白增添了不少思惟負載。