[譯]React如何區別class和function

原文 How Does React Tell a Class from a Function?javascript

譯註:html

一分鐘概覽——java

React最後採用了在React.Component上加入isReactComponent標識做爲區分。node

1.在這以前,考慮了ES6的區分方法,可是因爲Babel的存在,這個方法不可用。react

2.老是調用new,對於一些純函數組件不適用。並且對箭頭函數使用new會出錯。git

3.把問題約束到React組件下,經過斷定原型鏈來作,可是可能有多個React實例致使斷定出錯,因此在原型上添加了標識位,標識位是一個對象,由於早期Jest會忽略普通類型如Boolean型。github

4.API檢測也是可行的,可是API的發展沒法預測,每一個檢測都會帶來額外的損耗,因此不是主要作法,可是在如今版本里已經加入了render檢測,用來檢測prototype.render存在,可是prototype.isReactComponent不存在的場景,這樣會拋出一個warning。面試

如下正文。數組

思考一下下面這個使用function定義的Greeting組件:瀏覽器

function Greeting() {
  return Hello;
}

React也支持class定義:

class Greeting extends React.Component {
  render() {
    return Hello;
  }
}

(直到最近,這是惟一可使用相似state這種功能的方法。)
當你在使用<Greeting />組件時,其實並不關心它是怎麼定義的。

// Class or function — whatever.

可是React本身是關心這些不一樣的!
若是Greeting是一個函數,React須要去調用它:

// Your code
function Greeting() {
  return Hello;
}

// Inside React
const result = Greeting(props); // Hello

可是若是Greeting是類,React就須要用new關鍵字去實例化一個對象,而後馬上調用它的render方法。

// Your code
class Greeting extends React.Component {
  render() {
    return Hello;
  }
}

// Inside React
const instance = new Greeting(props); // Greeting {}
const result = instance.render(); // Hello

React有一個相同的目的——獲得一個渲染完畢的node(在這個例子裏,<p>Hello</p>)。可是若是定義Greeting決定了剩下的步驟。
因此React是如何知道一個組件是類仍是函數?
就像我以前的博客,你不須要知道這個東西對於React而言的效果。我一樣好幾點不瞭解這些。請不要把這個問題變成一個面試題。事實上,比起React,這篇博客更關注於JavaScript。
這篇博客寫給那些富有好奇心的讀者,他們想知道爲何React能以一種肯定的方式工做。你是這樣的人嗎?一塊兒深刻探討吧!
這是一段漫長的旅程。這篇博客不會寫不少關於React的東西,可是會一掠JavaScript自己的風采,諸如:new,this,class,箭頭函數,prototype,__proto__,instanceof,以及這些東西如何在JavaScript中合做。幸運的是,在你使用React的時候,你沒必要想太多這些事。


首先,咱們須要明白爲何區分函數和類如此重要。注意咱們怎麼使用new操做符去調用一個類:

// If Greeting is a function
const result = Greeting(props); // Hello

// If Greeting is a class
const instance = new Greeting(props); // Greeting {}
const result = instance.render(); // Hello

先來對new操做符作了什麼給出一個粗淺的定義。


之前,JavaScript沒有類的概念。然而,你也能夠用純函數去描述一種近似於類的模式。具體而言,你能夠在調用函數以前,添加new,就可使用任何相似於類構造器的函數了。

// Just a function
function Person(name) {
  this.name = name;
}

var fred = new Person('Fred'); // ✅ Person {name: 'Fred'}
var george = Person('George'); // 🔴 Won’t work

直到如今,你仍是能夠這麼寫,在調試工具裏試一下吧。
若是不使用new,直接調用Person('Fred'),函數內部的this就會指向一些全局變量,也沒什麼用了(例如:window或undefined)。因此咱們的代碼就會奔潰,或者作些蠢事像是設置了window.name
經過添加一個new操做符,就像告訴編譯器:「Hey,JavaScript,我知道Person只是一個函數,可是請僞裝它是一個類構造器。去建立一個實例對象,而後把this指向這個對象,這樣就能夠把this.name指向這個對象了。最後把這個對象的引用給我。」
new操做符大概作了這些事。

var fred = new Person('Fred'); // Same object as `this` inside `Person`

new操做符也讓全部Person.prototype的東西均可以被fred對象訪問。

function Person(name) {
  this.name = name;
}
Person.prototype.sayHi = function() {
  alert('Hi, I am ' + this.name);
}

var fred = new Person('Fred');
fred.sayHi();

這是你們在JavaScript直接支持類特性以前模擬的方法。


因此new在JavaScript中存在好久了,而class則是比較新的特性。咱們重寫這些代碼,來更加貼近咱們的想法。

class Person {
  constructor(name) {
    this.name = name;
  }
  sayHi() {
    alert('Hi, I am ' + this.name);
  }
}

let fred = new Person('Fred');
fred.sayHi();

對於語言和API設計而言,捉住開發者的意圖是很重要的。

若是你寫函數,JavaScript沒法猜想它是直接調用(如alert)仍是想對待構造器(如new Person())同樣對待它。若是對相似Person 這樣的函數忘記指定new操做符,會帶來使人費解的表現。

類語法讓咱們能夠告訴編譯器:「這不只是一個函數,它是一個類而且擁有一個構造器。」若是你忘了調用new ,JavaScript就會拋出一個錯誤。

let fred = new Person('Fred');
// ✅  If Person is a function: works fine
// ✅  If Person is a class: works fine too

let george = Person('George'); // We forgot `new`
// 😳 If Person is a constructor-like function: confusing behavior
// 🔴 If Person is a class: fails immediately

這就能夠是咱們及時發現一些古怪的錯誤,好比,this 被指向了window而不是咱們指望的george

然而,這也意味着React須要在實例任何類對象以前調用new。如同前面而言,若是少了這一步,就會拋出錯誤。

class Counter extends React.Component {
  render() {
    return Hello;
  }
}

// 🔴 React can't just do this:
const instance = Counter(props);

這是個大麻煩。

在查看React如何解決這個問題以前,應該清楚,大部分人爲了讓代碼能夠跑在舊瀏覽器裏,一般使用Babel或者其餘編譯器去處理相似class這種現代語法。因此在咱們的設計裏,必需要考慮編譯器。

在Babel早前的版本里,類可以不經過new去調用。然而,這個bug最後被修復了——經過生成下面這些代碼。

function Person(name) {
  // A bit simplified from Babel output:
  if (!(this instanceof Person)) {
    throw new TypeError("Cannot call a class as a function");
  }
  // Our code:
  this.name = name;
}

new Person('Fred'); // ✅ Okay
Person('George');   // 🔴 Can’t call class as a function

你也許在構建以後的bundle裏看到過這樣的代碼。這是全部_classCallCheck 函數所作的事情。(你能夠選擇「loose mode(寬鬆模式)」來使得編譯器繞過這些檢查,但可能會使得最終生成的class代碼很複雜。)


到目前爲止,你應該大概瞭解了有new和無new 的區別。

new Person() Person()
class thisPerson實例 🔴TypeError
function thisPerson實例 😳this指向windowundefined

這就是React正確調用組件的重要之處。若是經過class聲明組件,就必須使用new去調用它。

因此這樣React就能檢查是不是class了嗎?

沒那麼簡單!即便咱們能夠區別ES6 class和function,可是這樣並不能判斷Babel這樣的工具生成的代碼。對於瀏覽器而言,他們都只是函數而言。真是不走運。


好吧,那React只能每次都使用new了嗎?然而,這樣也不行。

對於通常的函數,若是經過new去調用,就會新建一個對象實例並將this 指向它。對於寫成構造器的函數(就像Person),這樣作是可行的,可是對於通常的函數而言,就很奇怪了。

function Greeting() {
  // We wouldn’t expect `this` to be any kind of instance here
  return Hello;
}

這樣雖然是能夠容忍的,可是還有兩個問題使得咱們不得不拋棄這種作法。


第一個問題是對箭頭函數使用new ,它並不會被Babel處理,直接加new會拋出一個錯誤。

const Greeting = () => Hello;
new Greeting(); // 🔴 Greeting is not a constructor

這種表現是符合預期的,也是符合箭頭函數設計的。箭頭函數的特殊點之一就是沒有本身的this值,它只能從最近的函數閉包內獲取this值。

class Friends extends React.Component {
  render() {
    const friends = this.props.friends;
    return friends.map(friend =>
      
    );
  }
}

OK,即便箭頭函數沒有本身的this值,可是也不意味着它徹底不能用做構造器!

const Person = (name) => {
  // 🔴 This wouldn’t make sense!
  this.name = name;
}

所以,JavaScript不容許使用new去調用箭頭函數。若是這麼作,就會盡早的拋出一個錯誤。這和不能不用new去調用一個類有點相似。

這個設計很好可是卻影響了咱們的計劃。React不能在全部東西上都加上new,由於這樣可能會破壞箭頭函數。咱們能夠經過檢測prototype去區分箭頭函數和普通函數。

(() => {}).prototype // undefined
(function() {}).prototype // {constructor: f}

可是這樣對Babel轉移後的函數並很差使。這也不算是大問題,可是還有一個問題讓咱們完全放棄了這個想法。


另外一個緣由在於使用new以後,React就沒法支持那些返回string這種基本類型的函數了。

function Greeting() {
  return 'Hello';
}

Greeting(); // ✅ 'Hello'
new Greeting(); // 😳 Greeting {}

這是new操做符的另外一個怪異設計。正如咱們以前看到的,new 告訴JavaScript引擎建立一個對象並把this 指向它,以後將它返回給咱們。

然而,JavaScript容許被new調用的函數重載,返回其餘對象。大概是在重用實例時,這種池模式比較方便。

// Created lazily
var zeroVector = null;

function Vector(x, y) {
  if (x === 0 && y === 0) {
    if (zeroVector !== null) {
      // Reuse the same instance
      return zeroVector;
    }
    zeroVector = this;
  }
  this.x = x;
  this.y = y;
}

var a = new Vector(1, 1);
var b = new Vector(0, 0);
var c = new Vector(0, 0); // 😲 b === c

然而,new 一樣會忽略那些非對象類型的返回值。若是隻是return一個string或者nunber,就像沒寫return同樣。

function Answer() {
  return 42;
}

Answer(); // ✅ 42
new Answer(); // 😳 Answer {}

若是用了new 調用函數,就沒有什麼辦法得到一個基本類型的return。因此,若是React一直用new 調用函數,直接返回string的函數將不能正常使用。

這是不可接受的,因此須要妥協一下。

到目前爲止,咱們學到了什麼?React須要使用new 去調用classes(包括Babel轉移後的),可是還須要不用new 直接調用通常函數和箭頭函數。可是卻沒有一種可靠的方法區分它們。

若是不能提出通用解法,是否是能夠把問題再細分一下?

當你使用class去定義一個組件,你通常會使用繼承React.Component ,而後去使用一些內建方法,好比this.setState()。與其檢測所有的class,不如只檢測React.Component的子類呢?

劇透:這也是React的作法。

通常而言,檢查子類通用的作法就是使用instance of。若是檢查Greeting 是否是React組件,就須要使用Greeting.prototype instanceof React.Component

class A {}
class B extends A {}

console.log(B.prototype instanceof A); // true

我知道你在想什麼。這裏發生了什麼?爲了解答這個問題,咱們須要明白JavaScript原型機制。

你可能據說過「原型鏈」。每一個JavaScript對象均可能有一個「prototype」。當調用fred.syaHi()時,若是fred 上沒有sayHi(),就會在它的原型上去尋找。若是沒找到,則繼續向上找,就像鏈條同樣。

使人費解的事,類或者函數的prototype屬性並非指向當前值得prototype。我沒開玩笑。

function Person() {}

console.log(Person.prototype); // 🤪 Not Person's prototype
console.log(Person.__proto__); // 😳 Person's prototype
// 更像是
__proto__.__proto__.__proto__
// 而不是
prototype.prototype.prototype

原型在函數或者類上究竟是啥?經過new實例化的對象都有__proto__屬性。

function Person(name) {
  this.name = name;
}
Person.prototype.sayHi = function() {
  alert('Hi, I am ' + this.name);
}

var fred = new Person('Fred'); // Sets `fred.__proto__` to `Person.prototype`

而後__proto__鏈展現了JavaScript如何尋找鏈上的屬性和方法。

fred.sayHi();
// 1. Does fred have a sayHi property? No.
// 2. Does fred.__proto__ have a sayHi property? Yes. Call it!

fred.toString();
// 1. Does fred have a toString property? No.
// 2. Does fred.__proto__ have a toString property? No.
// 3. Does fred.__proto__.__proto__ have a toString property? Yes. Call it!

實際上,在代碼裏幾乎不須要操做__proto__,除非須要調試原型鏈相關的東西。若是你想要將一些屬性在fred.__proto__,你應該把它放在Person.prototype。至少這是最初的設計。

瀏覽器曾經不會把__proto__屬性暴露出來,由於原型鏈被認爲是一個內部概念。一些瀏覽器添加了對__proto__的支持,後續艱難地標準化了,可是爲了支持Object.getPrototypeOf() 又會被移出標準。

我仍然以爲很困惑,一個屬性稱爲原型但不給你一個有用的原型(例如,fred.prototype是undefined,由於fred不是一個函數)。就我而言,我認爲最大的緣由是,,哪怕是有經驗的開發人員也經常會誤解JavaScript原型。


這篇博客太長了,已經講完80%了。繼續。

咱們知道對於obj.foo,JavaScript實際上去尋找objfoo,找不到再去obj.__proto__obj.__proto__.__proto__……

經過使用class,沒有必要直接去使用這個機制,extends在原型鏈下也能工做的很好。下面的例子講述了爲何React類實例能獲取像setState這樣的方法。

class Greeting extends React.Component {
  render() {
    return Hello;
  }
}

let c = new Greeting();
console.log(c.__proto__); // Greeting.prototype
console.log(c.__proto__.__proto__); // React.Component.prototype
console.log(c.__proto__.__proto__.__proto__); // Object.prototype

c.render();      // Found on c.__proto__ (Greeting.prototype)
c.setState();    // Found on c.__proto__.__proto__ (React.Component.prototype)
c.toString();    // Found on c.__proto__.__proto__.__proto__ (Object.prototype)

換句話說,當你使用class,一個實例的__proto__鏈和類層次一一對應。

// `extends` chain
Greeting
  → React.Component
    → Object (implicitly)

// `__proto__` chain
new Greeting()
  → Greeting.prototype
    → React.Component.prototype
      → Object.prototype

經過類層次和__proto__鏈的一一對應,咱們能夠循着原型鏈找到父級。

// `__proto__` chain
new Greeting()
  → Greeting.prototype // 🕵️ We start here
    → React.Component.prototype // ✅ Found it!
      → Object.prototype

x instanceof Y就是使用__proto__鏈進行查找。就是在x.__proto__鏈上尋找Y.prototype

正常狀況下,通常用來肯定實例的類型。

let greeting = new Greeting();

console.log(greeting instanceof Greeting); // true
// greeting (🕵️‍ We start here)
//   .__proto__ → Greeting.prototype (✅ Found it!)
//     .__proto__ → React.Component.prototype 
//       .__proto__ → Object.prototype

console.log(greeting instanceof React.Component); // true
// greeting (🕵️‍ We start here)
//   .__proto__ → Greeting.prototype
//     .__proto__ → React.Component.prototype (✅ Found it!)
//       .__proto__ → Object.prototype

console.log(greeting instanceof Object); // true
// greeting (🕵️‍ We start here)
//   .__proto__ → Greeting.prototype
//     .__proto__ → React.Component.prototype
//       .__proto__ → Object.prototype (✅ Found it!)

console.log(greeting instanceof Banana); // false
// greeting (🕵️‍ We start here)
//   .__proto__ → Greeting.prototype
//     .__proto__ → React.Component.prototype 
//       .__proto__ → Object.prototype (🙅‍ Did not find it!)

它也能夠用來肯定一個類是否繼承自另外一個類。

console.log(Greeting.prototype instanceof React.Component);
// greeting
// .__proto__ → Greeting.prototype (🕵️‍ We start here)
// .__proto__ → React.Component.prototype (✅ Found it!)
// .__proto__ → Object.prototype

這下咱們能夠肯定一個組件是用函數聲明仍是類聲明瞭。

雖然這些東西不是React作的。

須要注意的是,instanceof不能用來識別頁面上繼承自兩個React基類的實例。在同一個頁面上,有兩個React實例,是一個錯誤的設計,可是歷史包袱畢竟可能存在,因此仍是要避免在這種狀況下使用instanceof。(經過使用Hooks,咱們可能須要強制維持兩份環境了。)

另外一種方法能夠檢測render() 的存在,可是有個問題,沒法預測往後API的變化。每次檢測都要花費時間,不但願之後API發生變化以後,又加一個。並且,若是實例上聲明瞭render(),也會繞過這個檢測。

因此,React在基類上增長了一個特殊的標識。React檢測這個標識的存在,這樣區別是不是React Component。

起初,這個標識依賴於React.Component自己。

// Inside React
class Component {}
Component.isReactClass = {};

// We can check it like this
class Greeting extends Component {}
console.log(Greeting.isReactClass); // ✅ Yes

然而,有點類實現不會拷貝靜態屬性,或者實現了一個不標準的__proto__鏈,因此傳着傳着,這個標識就丟了。

這也是爲何React把這個標識移到了React.Component.prototype

// Inside React
class Component {}
Component.prototype.isReactComponent = {};

// We can check it like this
class Greeting extends Component {}
console.log(Greeting.prototype.isReactComponent); // ✅ Yes

你也許會奇怪爲何標識是一個對象而不是Boolean型。實際上沒多大區別,可是在早期的Jest版本中,有自動Mock的機制。Mock後的數據會忽略基本類型的屬性,會破壞檢測。感謝Jest。

isReactComponent至今仍在使用

若是沒有繼承React.Component,React在原型上沒有發現isReactComponent 標識,就會像對待普通類同樣對待它。如今就知道爲何Cannot call a class as a function問題下得票最多的回答建議添加extends React.Component。最後,一個警告已經被加入到React中,用來檢測prototype.render存在,可是prototype.isReactComponent不存在的場景。


你可能以爲這是一個關於替換的故事。實際的解決辦法很簡單,可是,我還須要解釋爲何要選擇這個方案,以及還存在哪些別的選擇。

以個人經驗,這對於library級別的API來講,是很常見的。爲了讓API簡單易用,你經常須要考慮語義(也許在一些語言裏,還包括將來方向),runtime性能,編譯與否,時間成本,生態,打包解決方案,及時warning,還有不少事情。最後的方案不必定優雅,但必定經得起考驗。

若是API設計得很成功,這些過程對於用戶就是透明的。他們能夠專一於開發APP。

可是若是你很好奇,能幫助你理解它如何工做也很棒。

相關文章
相關標籤/搜索