什麼是繼承呢?
繼承(英語:inheritance)是物件導向軟體技術當中的一個概念。如果一個類別A“繼承自”另一個類別B,就把這個A稱為“B的子類別”,而把B稱為“A的父類別”也可以稱“B是A的超類”。繼承可以使得子類別具有父類別的各種屬性和方法,而不需要再次編寫相同的程式碼。在令子類別繼承父類別的同時,可以重新定義某些屬性,並重寫某些方法,即覆蓋父類別的原有屬性和方法,使其獲得與父類別不同的功能。另外,為子類別追加新的屬性和方法也是常見的做法。
es 5 中經典的繼承
什麼是原型鏈
instance.__proto__ —> B.prototype
B.prototype.__proto__ —> A.prototype
以此類推
例項訪問方法就是通過 __proto__ 來訪問類的例項物件上的方法,其追蹤過程也是通過 B.prototype.__proto__ 來向父類追蹤
1.物件有屬性 __proto__, 指向該物件的建構函式的原型物件。
2.方法除了有屬性 __proto__, 還有屬性prototype,prototype指向該方法的原型物件。
(類/方法本身的 __proto__ 指向 js 方法本身的定義,無意義)
es5 的繼承實現
- 方案一
function inherits (Parent, Child) {
Child.prototype = new Parent() // Child.prototype.__proto__ === Parent.prototype
Child.prototype.constructor = Child
}複製程式碼
-
當父類建構函式含參時不可用*
-
方案二
function inherits (Parent, Child) {
let Temp = function () {}
Temp.prototype = Parent.prototype
Child.prototype = new Temp()
Child.prototype.constructor = Child
}複製程式碼
- 方案三
function inherits (Parent, Child) {
Child.prototype = Object.create(Parent.prototype) // Child.prototype = Parent.prototype, 這裡是直接將原型拷貝了。此時 Child 就是擴充後的 Parent
Child.prototype.constructor = Child
}複製程式碼
使用:
function Student(props) {
this.name = props.name || `Unnamed`
}
function PrimaryStudent(props) {
Student.call(this, props) // 必須要執行的一段程式碼,相當於 class 建構函式中的 super(props), 主要用來生成父類例項(繼承相當於是對父類例項的進一步加工修改)
this.grade = props.grade || 1
}
// 實現原型繼承鏈:
inherits(PrimaryStudent, Student)複製程式碼
es 5 中繼承的實際應用
此處我們需要 hack 的是 commonmark 的 HtmlRenderer 類中的一段渲染函式。 commonmark 是一個 markdown 渲染庫
import commonmark from `commonmark`
const MyRenderer = function (options) { // 建構函式,繫結 this
return commonmark.HtmlRenderer.call(this, options)
}
MyRenderer.prototype = Object.create(commonmark.HtmlRenderer.prototype) // 使用方案三實現繼承
MyRenderer.prototype.image = function (node, entering) {
node.destination = this.options.convertHref(node.destination) // 任何形式的 hack 程式碼
return commonmark.HtmlRenderer.prototype.image.call(this, node, entering) // 使用 hack 好的新資料、this 對原有方法呼叫返回結果
}複製程式碼
箭頭函式
箭頭函式 是 ecma 標準 中的官方定義,主要是為了解決函式編譯時函式中 this 的指向問題。而在 Java8 中則對應有 Lambda 表示式,而它主要是解決了單函式介面的寫法簡潔性問題,在使用 Java7 的 IDE 中,原有的寫法也會被處理為箭頭函式的顯示方式。
箭頭函式的相應等價寫法
在箭頭函式之前,我們都使用匿名函式搭配 bind(this)
的方式來繫結編譯時上下文。因此經常會出現一下程式碼:
(function () {...}).bind(this)複製程式碼
這樣就把當前匿名函式繫結為上下文的 this。
(普通函式 or 匿名函式中的 this 指向 window 或者 undefined,而只有在執行時指定呼叫者時,才會指向呼叫者)
而箭頭函式只需要
() => {...}複製程式碼
因為一些問題的進一步優化
硬程式碼的每一次執行,都會建立一個新的函式。
如果出現一些需要函式唯一性的場景,濫用箭頭函式產生的弊端逐步顯露。
典型場景——“訂閱與反訂閱”
BackAndroid.addEventListener(`hardwareBackPress`, this.handleBack)複製程式碼
BackAndroid.removeEventListener(`hardwareBackPress`, this.handleBack)複製程式碼
RN 中對返回事件的監聽,由原始碼可知,在反訂閱的執行中是由第一個引數(標記量)和第二個標記量(函式引用)共同決定的,此時如果因為函式的執行而產生了一個新的函式,反訂閱會失敗的。
此時就有了將該函式在單次執行的生命週期中存入當前類的例項 this 上(比較通用的有 constructor)。
constructor () {
super()
this.handleBack = () => {}
or
this.handleBack = (function () {}).bind(this)
}複製程式碼
再後來,React 又加入了在 class 中對箭頭函式的支援,因此可以直接這麼寫:
class A {
arrowFunction = () => {}
}複製程式碼
(注意,這種寫法還是相當於宣告在了例項 this 上,而非 A 的原型 prototype 上)
class 繼承中的箭頭函式
之前為了擴充一個類,突發奇想想到了 es6 的 class (雖然知道它就是 es5 原型鏈繼承的語法糖),憑藉對 C++ 以及 Java 繼承經驗的類比,最後成功的,跌入了 es6 的深坑:
- 第一步,我們使用普通建立類的方式宣告瞭一個類 A
class A {
files = []
startUpload = () => {
...
}
}複製程式碼
這是一個檔案上傳的簡易類,此時我們又一個需求,就是在某種情況下需要實現在 startUpload 之前對 files 進行重新排序,為了避免強耦合,本來打算用組合或者注入高階函式的方式實現,最後還是選擇了繼承。
class B extends A {
sort () {
...
}
startUpload = () => {
this.sort()
super.startUpload()
}
}複製程式碼
然而問題出現了,我們使用圖中的方式根本無法實現真正意義上的擴充,問題就出在了箭頭函式上:
- 第二步分析:
箭頭函式是被宣告在了對應的 this 上,因此在子類上二次宣告 startUpload 方法是對父類 this 上屬性的覆蓋,而非並存。而且在語法層面上 es6 根本不允許在箭頭函式上訪問關鍵字super
。
因此解決方案就是將箭頭函式全部替換為對應的普通方法,將方法放到類的原型上,這樣就可以實現了方法的繼承,而非覆蓋。
總結
- 子類中的非靜態方法中,super 指向父類原型。
- 靜態方法中,super 指向父類。
- 由於
super()
方法的執行實際上是父類.prototype.constructor.call(this, props)
等價於父類.call(this, props)
。 父類從構造開始,this 就被指定為了子類的 this,此時可以視作父子類 this 的合併。此時 super === this,因此賦值的時候是 this,而在取值的時候則遵循 1 。 - 在生成例項的時候,會對子類父類 this 上的屬性進行合併,如果有重名的,則子類覆蓋父類。
- 同箭頭函式一樣,所有直接宣告在類體上的屬性變數都是在 this 上的,類似於 state、箭頭函式的宣告。
- 使用例項訪問物件內容時,優先訪問例項 this 上的內容,其次訪問子類原型上的內容,在找不到對應內容後,再一層一層以原型鏈為線索向上查詢內容。
- 在普通方法中使用 this 訪問內容時,和使用子類例項等價。