探索 React 中 es6 的繼承機制

Pober_Wong發表於2019-02-25

什麼是繼承呢?

繼承(英語: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
    因此解決方案就是將箭頭函式全部替換為對應的普通方法,將方法放到類的原型上,這樣就可以實現了方法的繼承,而非覆蓋。

總結

  1. 子類中的非靜態方法中,super 指向父類原型。
  2. 靜態方法中,super 指向父類。
  3. 由於 super() 方法的執行實際上是 父類.prototype.constructor.call(this, props) 等價於 父類.call(this, props)。 父類從構造開始,this 就被指定為了子類的 this,此時可以視作父子類 this 的合併。此時 super === this,因此賦值的時候是 this,而在取值的時候則遵循 1 。
  4. 在生成例項的時候,會對子類父類 this 上的屬性進行合併,如果有重名的,則子類覆蓋父類。
  5. 同箭頭函式一樣,所有直接宣告在類體上的屬性變數都是在 this 上的,類似於 state、箭頭函式的宣告。
  6. 使用例項訪問物件內容時,優先訪問例項 this 上的內容,其次訪問子類原型上的內容,在找不到對應內容後,再一層一層以原型鏈為線索向上查詢內容。
  7. 在普通方法中使用 this 訪問內容時,和使用子類例項等價。

相關文章