[譯]為什麼要寫 super(props)

第一秩序發表於2019-02-01

原文:overreacted.io/why-do-we-w…

我聽說 Hooks 最近很火。諷刺的是,我想用一些關於 class 元件的有趣故事來開始這篇文章。你覺得如何?

本文中這些坑對於你正常使用 React 並不是很重要。 但是假如你想更深入的瞭解它的運作方式,就會發現實際上它們很有趣。

開始第一個。


首先在我的職業生涯中寫過的 super(props) 自己都記不清:

class Checkbox extends React.Component {
  constructor(props) {
    super(props);
    this.state = { isOn: true };
  }
  // ...
}
複製程式碼

當然,在類欄位提案 (class fields proposal) 中建議讓我們跳過這個開頭:

class Checkbox extends React.Component {
  state = { isOn: true };
  // ...
}
複製程式碼

在2015年 React 0.13 增加對普通類的支援時,曾經打算用這樣的語法。定義 constructor 和呼叫 super(props) 始終是一個臨時的解決方案,可能要等到類欄位能夠提供在工程學上不那麼反人類的替代方案。

不過還是讓我們回到前面這個例子,這次只用ES2015的特性:

class Checkbox extends React.Component {
  constructor(props) {
    super(props);
    this.state = { isOn: true };
  }
  // ...
}
複製程式碼

為什麼我們要呼叫super? 可以呼叫它嗎? 如果必須要呼叫,不傳遞prop引數會發生什麼? 還有其他引數嗎? 接下來我們試一試:


在 JavaScript 中,super 指的是父類的建構函式。(在我們的示例中,它指向React.Component 的實現。)

重要的是,在呼叫父類建構函式之前,你不能在建構函式中使用this。 JavaScript 是不會讓你這樣做的:

class Checkbox extends React.Component {
  constructor(props) {
    // ? 這裡還不能用 `this` 
    super(props);
    // ✅ 現在可以用了
    this.state = { isOn: true };
  }
  // ...
}
複製程式碼

為什麼 JavaScript 在使用this之前要先強制執行父建構函式,有一個很好的理由能夠解釋。 先看下面這個類的結構:

class Person {
  constructor(name) {
    this.name = name;
  }
}

class PolitePerson extends Person {
  constructor(name) {
    this.greetColleagues(); // ? 這行程式碼是無效的,後面告訴你為什麼
    super(name);
  }
  greetColleagues() {
    alert('Good morning folks!');
  }
}
複製程式碼

如果允許在呼叫 super 之前使用this的話。一段時間後,我們可能會修改 greetColleagues,並在提示訊息中新增Personname

 greetColleagues() {
    alert('Good morning folks!');
    alert('My name is ' + this.name + ', nice to meet you!');
  }
複製程式碼

但是我們忘記了 super() 在設定 this.name 之前先呼叫了 this.greetColleagues()。 所以此時 this.name 還沒有定義! 如你所見,像這樣的程式碼很難想到問題出在哪裡。

為了避免這類陷阱,JavaScript 強制要求:如果想在建構函式中使用this,你必須首先呼叫super。 先讓父類做完自己的事! 這種限制同樣也適用於被定義為類的 React 元件:

  constructor(props) {
    super(props);
    // ✅ 在這裡可以用 `this`
    this.state = { isOn: true };
  }
複製程式碼

這裡又給我們留下了另一個問題:為什麼要傳 props 引數?


你可能認為將props傳給super是必要的,這可以使 React.Component 的建構函式可以初始化this.props

// Inside React
class Component {
  constructor(props) {
    this.props = props;
    // ...
  }
}
複製程式碼

這與正確答案很接近了 —— 實際上它就是這麼做的

但是不知道為什麼,即便是你呼叫 super 時沒有傳遞 props 引數,仍然可以在 render 和其他方法中訪問this.props。 (不信你可以親自去試試!)

這是究竟是為什麼呢? 事實證明,在呼叫建構函式後,React也會在例項上分配 props

  // Inside React
  const instance = new YourComponent(props);
  instance.props = props;
複製程式碼

因此,即使你忘記將props傳給 super(),React 仍然會在之後設定它們。 這是有原因的。

當 React 新增對類的支援時,它不僅僅增加了對 ES6 類的支援。它的目標是儘可能廣泛的支援類抽象。 目前還不清楚 ClojureScript、CoffeeScript、ES6、Fable、Scala.js、TypeScript或其他解決方案是如何相對成功地定義元件的。 所以 React 故意不關心是否需要呼叫 super() —— 即使是ES6類。

那麼這是不是就意味著你可以寫 super() 而不是super(props)呢?

可能不行,因為它仍然是令人困惑的。 當然,React 稍後會在你的建構函式執行後分配 this.props, 但是在呼叫 super() 之後和建構函式結束前這段區間內 this.props 仍然是未定義的:

// Inside React
class Component {
  constructor(props) {
    this.props = props;
    // ...
  }
}

// Inside your code
class Button extends React.Component {
  constructor(props) {
    super(); // ? 我們忘記了傳遞 props 引數
    console.log(props);      // ✅ {}
    console.log(this.props); // ? undefined 
  }
  // ...
}
複製程式碼

如果這種情況發生在從建構函式呼叫的某個方法中,可能會給除錯工作帶來很大的麻煩。 這就是為什麼我建議總是呼叫 super(props) ,即使在沒有必要的情況之下:

class Button extends React.Component {
  constructor(props) {
    super(props); // ✅ 傳遞了 props 引數
    console.log(props);      // ✅ {}
    console.log(this.props); // ✅ {}
  }
  // ...
}
複製程式碼

這樣就確保了能夠在建構函式退出之前設定好 this.props


最後一點是長期以來 React 使用者總是感到好奇的。

你可能已經注意到,當你在類中使用Context API時(無論是舊版的 contextTypes 或在 React 16.6中新新增的 contextType API),context 會作為第二個引數傳遞給建構函式。

那麼為什麼我們不寫成 super(props, context) 呢? 我們可以這樣做,但是使用 context的頻率較低,所以這個坑並沒有那麼多影響。

根據類欄位提案的說明,這些坑大部分都會消失。 如果沒有顯式建構函式,則會自動傳遞所有引數。 這允許在像 state = {} 這樣的表示式中包含對 this.propsthis.context 的引用(如果有必要的話)。

而有了 Hooks 之後,我們甚至不再有 superthis 。 不過這是另外一個的話題了。

相關文章