寫在前言前面的話
雖說標題是繼承,但繼承這塊的涉及的知識點不僅僅只是繼承,所以這塊我會分成兩個部分來講:
- 第一部分主講原型以及原型鏈
- 第二部分主講繼承的幾種方式
分兩節講有以下兩個原因:
- 需要了解繼承必須對原型和原型鏈有深刻的瞭解,分開講好消化
- 最近孩子快出生,需要更多的時間翻閱資料來保證文章的質量
前言
繼承於我們前端來說絕對是非常熟悉也必須熟悉的一個高頻必懂知識點。熟悉到只要是面試一定會有關於繼承的問題;而且原始碼中繼承的使用也隨處可見。
可依舊有很多前端對繼承的實現和應用沒有一個整體的把握。追其原因無非有二:
- ECMAScript 繼承的實現方法區別於其他基於類的實現繼承的物件導向(Object Oriented)語言。
- 工作中即使對如何實現繼承一知半解,也一點都不耽誤寫邏輯程式碼。
無論由於哪一個原因,建議請儘快弄懂繼承的實現和應用,否則你可能會如同你的表情包一樣——流下了沒有技術的淚水。
接下來我會盡我所能講清楚繼承這個概念,並結合相關經典圖文做輔助解釋。
在講 ECMAScript 繼承的概念之前,我先說下類和原型的概念。
類與原型
類
講 ECMAScript 繼承的概念之前,我先說下類的概念。(如果接觸過 Java 或者是 C++ 的話,我們就知道 Java(C++)的繼承都是基於類的繼承)。
類: 是物件導向(Object Oriented)語言實現資訊封裝的基礎,稱為類型別。每個類包含資料說明和一組運算元據或傳遞訊息的函式。類的例項稱為物件。
類: 是描述了一種程式碼的組織結構形式,一種在軟體中對真實世界中問題領域的建模方法。
類的概念這裡我就不再擴充套件,感興趣的同學可以自行查閱書籍。接下來我們重點講講原型以及原型鏈。
原型
JavaScript 這門語言沒有類的概念,所以 JavaScript 並非是基於類的繼承,而是基於原型的繼承。(主要是借鑑 Self 語言原型(prototype
)繼承機制)。
注意:ES6 中的 class 關鍵字和 OO 語言中的類的概念是不同的,下面我會講到。ES6 的 class 其內部同樣是基於原型實現的繼承。
JavaScript 摒棄類轉而使用原型作為實現繼承的基礎,是因為基於原型的繼承相比基於類的繼承上在概念上更為簡單。首先我們明確一點,類存在的目的是為了例項化物件,而 JavaScript 可以直接通過物件字面量語法輕鬆的建立物件。
每一個函式,都有一個 prototype
屬性。
所有通過函式 new
出來的物件,這個物件都有一個 __proto__
指向這個函式的 prototype
。
當你想要使用一個物件(或者一個陣列)的某個功能時:如果該物件本身具有這個功能,則直接使用;如果該物件本身沒有這個功能,則去 __proto__
中找。
1. prototype
[顯式原型]
prototype
是一個顯式的原型屬性,只有函式才擁有該屬性。
每一個函式在建立之後都會擁有一個名為prototype
的屬性,這個屬性指向函式的原型物件。( 通過Function.prototype.bind
方法構造出來的函式是個例外,它沒有prototype
屬性 )。
prototype
是一個指標,指向的是一個物件。比如 Array.ptototype
指向的就是 Array 這個函式的原型物件。
在控制檯中列印 console.log(Array.prototype)
裡面有很多方法。這些方法都以事先內建在 JavaScript 中,直接呼叫即可。上面我標紅了兩個特別的屬性 constructor
和 __proto__
。這兩個屬性接下來我都會講。
我們現在寫一個 function noWork(){}
函式。
當我寫了一個 noWork
這個方法的時候,它自動建立了一個 prototype
指標屬性(指向原型物件)。而這個被指向的原型物件自動獲得了一個 constructor
(建構函式)。細心的同學一定發現了:constructor
指向的是 noWork
。
noWork.prototype.constructor === noWork // true
複製程式碼
一個函式的原型物件的建構函式是這個函式本身
如圖:
tips: 圖中列印的 Array 的顯式原型物件中的這些方法你都知道嗎?要知道陣列也是非常重要的一部分哦 ~ 咳咳咳,這是考試重點。
2. __proto__
[隱式原型]
prototype
理解起來不難,__proto__
理解起來就會比 prototype
稍微複雜一點。不過當你理解的時候你會發現,這個過程真的很有趣。下面我們就講講 __proto__
。
其實這個屬性指向了 `[[prototype]]`,但是 `[[prototype]]` 是內部屬性,我們並不能訪問到,所以使用 `__proto__` 來訪問。
我先給個有點繞的定義:
__proto__
指向了建立該物件的建構函式的顯式原型。
我們現在還是使用 noWork
這個例子來說。我們發現 noWork
原型物件中還有另一個屬性 __proto__
。
我們先列印這個屬性:
我們發現這個 __proto__
指向的是 Object.prototype
。
我聽到有人在問為什麼?
- 因為這個
__proto__.constructor
指向的是Object
。 - 我們知道:一個函式的原型物件的建構函式是這個函式本身。
- 所以這個
__proto__.constructor
指向的是Object.prototype.constructor
。 - 進而
__proto__
指向的是Object.prototype
。
如圖:
我們來驗證一下:
至於為什麼是指向 Object? 因為所有的引用型別預設都是繼承 Object 。
作用
- 顯式原型:用來實現基於原型的繼承與屬性的共享。
- 隱式原型:構成原型鏈,同樣用於實現基於原型的繼承。 舉個例子,當我們使用
noWork
這個物件中的toString()
屬性時,在noWork
中找不到,就會沿著__proto__
依次查詢。
3. new 操作符
當我們使用 new 操作符時,生成的例項物件擁有了 __proto__
屬性。即在 new 的過程中,新物件被新增了 __proto__
並且連結到建構函式的原型上。
new 的過程
- 新生成了一個物件
- 連結到原型
- 繫結 this
- 返回新物件
Function.__proto__ === Function.prototype
難道這代表著 Function
自己產生了自己? 要說明這個問題我們先從 Object
說起。
我們知道所有物件都可以通過原型鏈最終找到 Object.prototype
,雖然 Object.prototype
也是一個物件,但是這個物件卻不是 Object
創造的,而是引擎自己建立了 Object.prototype
。 所以可以這樣說:
所有例項都是物件,但是物件不一定都是例項。
接下來我們來看 Function.prototype
這個特殊的物件:
列印這個物件,會發現這個物件其實是一個函式。我們知道函式都是通過 new Function()
生成的,難道 Function.prototype
也是通過 new Function()
產生的嗎?這個函式也是引擎自己建立的。
首先引擎建立了
Object.prototype
,然後建立了Function.prototype
,並且通過__proto__
將兩者聯絡了起來。
這就是為什麼 Function.prototype.bind()
沒有 prototype
屬性。因為 Function.prototype
是引擎建立出來的物件,引擎認為不需要給這個物件新增 prototype
屬性。
對於為什麼
Function.__proto__
會等於Function.prototype
?
我看到的一個解釋是這樣的:
其他所有的建構函式都可以通過原型鏈找到Function.prototype
,並且function Function()
本質也是一個函式,為了不產生混亂就將function Function()
的__proto__
聯絡到了Function.prototype
上。
繼承的幾種方式
未完待續
參考
- 《JavaScript 高階程式設計》
- 《你不知道的 JavaScript – 上》
- 《JavaScript 語言精粹》
- ~zepto設計和原始碼分析
前端詞典系列
《前端詞典》這個系列會持續更新,每一期我都會講一個出現頻率較高的知識點。希望大家在閱讀的過程當中可以斧正文中出現不嚴謹或是錯誤的地方,本人將不勝感激;若通過本系列而有所得,本人亦將不勝欣喜。
內容: 前端以及網路相關知識點的介紹並加以實際應用作為輔助。
目的: 這個系列的文章可以對讀者起到一點幫助,解開一些迷惑。
希望各位多指點一二,不吝賜教。
下期預告
【前端詞典】繼承(二) – “回”的幾種寫法