本週面試題一覽:
- 原型鏈繼承的基本思路是什麼?有什麼優缺點?
- 借用建構函式和組合繼承基本思路是什麼?有什麼優缺點?
- 原型式繼承的基本思路是什麼?有什麼優缺點?
- 寄生式繼承的基本思路是什麼?有什麼優缺點?
- 寄生組合式繼承的基本思路是什麼?有什麼優缺點?
本週是繼承專題,在開始之前,需要先了解建構函式、原型和原型鏈的相關知識。
更多優質文章可戳: github.com/YvetteLau/B…
建構函式
建構函式和普通函式的區別僅在於呼叫它們的方式不同,任何函式,只要通過 new
操作符來呼叫,那它就可以作為建構函式;任何函式,如果不通過 new
操作符來呼叫,那麼它就是一個普通函式。
例項擁有 constructor(建構函式)
屬性,該屬性返回建立例項物件的建構函式。
有一點需要說明的是,除了基本資料型別的 constructor
外( null
和 undefined
無 constructor
屬性),constructor
屬性是可以被重寫的。因此檢測物件型別時,instanceof
操作符比 constructor
更可靠一些。
原型
我們建立的每個函式都有 prototype
屬性,這個屬性指向函式的原型物件。原型物件的用途是包含可以由特定型別的所有例項共享的屬性和方法。
在預設情況下,所有原型物件都會自動獲得一個 constructor
屬性,這個屬性包含一個指向 prototype
屬性所在函式的指標。
當呼叫建構函式建立一個新例項後,該例項的內部將包含一個指標,指向建構函式的原型物件(可以通過例項的 __proto__
來訪問建構函式的原型物件)。
例項.__proto__ === 建構函式.prototype
console.log(Object.prototype.__proto__ === null) //true
console.log(Object.__proto__ === Function.prototype) //true
console.log(Function.prototype.__proto__ === Object.prototype) //true
複製程式碼
原型鏈
簡單回顧一下建構函式、原型和例項的關係:
每個建構函式都有一個原型物件,原型物件都包含一個指向建構函式的指標,而例項都包含一個可以指向原型物件的內部指標(可以通過 __proto
訪問)。
假如我們讓原型物件等於另一個型別的例項,那麼此時原型物件包含一個指向另一個原型的指標,相應地,另一個原型中也包含著一個指向另一個建構函式的指標。假如另一個原型又是另一個型別的例項,那麼上述關係仍然成立,如此層層遞進,就構成了例項與原型的鏈條,這就是原型鏈的基本概念。
一圖勝萬言:
呼叫 instance.getType()
會呼叫以下的搜尋步驟:
- 搜尋
instance
例項 - 搜尋
SimType.prototype
- 搜尋
SubType.prototype
- 搜尋
SuperType.prototype
,找到了getType
方法
在找不到屬性或方法的情況下,搜尋過程總是要一環一環地前行到原型鏈的末端才會停下來。
所有引用型別都繼承了 Object
,這個繼承也是通過原型鏈實現的。如果在 SuperType.prototype
還沒有找到 getType
,就會到 Object.prototype
中找(圖中少畫了一環)。
原型鏈繼承
原型鏈繼承的基本思想是利用原型讓一個引用型別繼承另一個引用型別的屬性和方法。
如 SubType.prototype = new SuperType()
;
可以看出 colors
屬性會被所有的例項共享(instance1、instance2、...)。
缺點:
- 通過原型來實現繼承時,原型會變成另一個型別的例項,原先的例項屬性變成了現在的原型屬性,該原型的引用型別屬性會被所有的例項共享。
- 在建立子型別的例項時,沒有辦法在不影響所有物件例項的情況下給超型別的建構函式中傳遞引數。
借用建構函式
借用建構函式的技術,其基本思想為:
在子型別的建構函式中呼叫超型別建構函式。
優點:
- 可以向超類傳遞引數
- 解決了原型中包含引用型別值被所有例項共享的問題
缺點:
- 方法都在建構函式中定義,函式複用無從談起,另外超型別原型中定義的方法對於子型別而言都是不可見的。
組合繼承
組合繼承指的是將原型鏈和借用建構函式技術組合到一塊,從而發揮二者之長的一種繼承模式。基本思路:
使用原型鏈實現對原型屬性和方法的繼承,通過借用建構函式來實現對例項屬性的繼承,既通過在原型上定義方法來實現了函式複用,又保證了每個例項都有自己的屬性。
缺點:
- 無論什麼情況下,都會呼叫兩次超型別建構函式:一次是在建立子型別原型的時候,另一次是在子型別建構函式內部。
優點:
- 可以向超類傳遞引數
- 每個例項都有自己的屬性
- 實現了函式複用
原型式繼承
原型繼承的基本思想:
藉助原型可以基於已有的物件建立新物件,同時還不必因此建立自定義型別。
在 object()
函式內部,先穿甲一個臨時性的建構函式,然後將傳入的物件作為這個建構函式的原型,最後返回了這個臨時型別的一個新例項,從本質上講,object()
對傳入的物件執行了一次淺拷貝。
ECMAScript5通過新增 Object.create()
方法規範了原型式繼承。這個方法接收兩個引數:一個用作新物件原型的物件和(可選的)一個為新物件定義額外屬性的物件(可以覆蓋原型物件上的同名屬性),在傳入一個引數的情況下,Object.create()
和 object()
方法的行為相同。
在沒有必要建立建構函式,僅讓一個物件與另一個物件保持相似的情況下,原型式繼承是可以勝任的。
缺點:
同原型鏈實現繼承一樣,包含引用型別值的屬性會被所有例項共享。
寄生式繼承
寄生式繼承是與原型式繼承緊密相關的一種思路。寄生式繼承的思路與寄生建構函式和工廠模式類似,即建立一個僅用於封裝繼承過程的函式,該函式在內部已某種方式來增強物件,最後再像真地是它做了所有工作一樣返回物件。
基於 person
返回了一個新物件 -—— person2
,新物件不僅具有 person
的所有屬性和方法,而且還有自己的 sayHi()
方法。在考慮物件而不是自定義型別和建構函式的情況下,寄生式繼承也是一種有用的模式。
缺點:
- 使用寄生式繼承來為物件新增函式,會由於不能做到函式複用而效率低下。
- 同原型鏈實現繼承一樣,包含引用型別值的屬性會被所有例項共享。
寄生組合式繼承
所謂寄生組合式繼承,即通過借用建構函式來繼承屬性,通過原型鏈的混成形式來繼承方法,基本思路:
不必為了指定子型別的原型而呼叫超型別的建構函式,我們需要的僅是超型別原型的一個副本,本質上就是使用寄生式繼承來繼承超型別的原型,然後再將結果指定給子型別的原型。寄生組合式繼承的基本模式如下所示:
- 第一步:建立超型別原型的一個副本
- 第二步:為建立的副本新增
constructor
屬性 - 第三步:將新建立的物件賦值給子型別的原型
至此,我們就可以通過呼叫 inheritPrototype
來替換為子型別原型賦值的語句:
優點:
只呼叫了一次超類建構函式,效率更高。避免在SuberType.prototype
上面建立不必要的、多餘的屬性,與其同時,原型鏈還能保持不變。
因此寄生組合繼承是引用型別最理性的繼承正規化。
ES6 繼承
Class
可以通過extends關鍵字實現繼承,如:
對於ES6的 class
需要做以下幾點說明:
- 類的資料型別就是函式,類本身就指向建構函式。
console.log(typeof SuperType);//function
console.log(SuperType === SuperType.prototype.constructor); //true
複製程式碼
- 類的內部所有定義的方法,都是不可列舉的。(ES5原型上的方法預設是可列舉的)
Object.keys(SuperType.prototype);
複製程式碼
-
constructor
方法是類的預設方法,通過new
命令生成物件例項時,自動呼叫該方法。一個類必須有constructor
方法,如果沒有顯式定義,一個空的constructor
方法會被預設新增。 -
Class
不能像建構函式那樣直接呼叫,會丟擲錯誤。
使用 extends
關鍵字實現繼承,有一點需要特別說明:
- 子類必須在
constructor
中呼叫super
方法,否則新建例項時會報錯。如果沒有子類沒有定義constructor
方法,那麼這個方法會被預設新增。在子類的建構函式中,只有呼叫super
之後,才能使用this
關鍵字,否則報錯。這是因為子類例項的構建,基於父類例項,只有super方法才能呼叫父類例項。
參考文章:
[1] [JavaScript高階程式設計第六章]
[2] ES6 Class
謝謝各位小夥伴願意花費寶貴的時間閱讀本文,如果本文給了您一點幫助或者是啟發,請不要吝嗇你的贊和Star,您的肯定是我前進的最大動力。 github.com/YvetteLau/B…
本文中的程式碼使用了圖片,如果您想直接複製程式碼,請移步 【Step-By-Step】高頻面試題深入解析 / 週刊06