(譯)React是如何區分Class和Function?

原文地址: how-does-react-tell-a-class-from-a-functionhtml

本文地址: React是如何區分Class和Function?react

邊看邊翻譯 花了2h+... 若是你以爲讀起來還算通順不費事 那也算我爲你們作了一點小貢獻吧git

React調用二者的不一樣之處

一塊兒來看下這個 function 類型的 Greeting組件:es6

function Greeting() {
  return <p>Hello</p>;
}
複製代碼

React 一樣支持將它定義爲 class 類型:github

class Greeting extends React.Component {
  render() {
    return <p>Hello</p>;
  }
}
複製代碼

(直到最近 hooks-intro,這是使用state等特性的惟一方法。)數組

當你想渲染<Greeting />組件時,你沒必要關心它是如何定義的:瀏覽器

//類或者函數,均可以
<Greeting />
複製代碼

可是,做爲 React自己 是會認爲這兩個是有不一樣之處的。babel

若是Greeting是一個函數,React 只須要直接調用它:ecmascript

// 你的代碼
function Greeting() {
  return <p>Hello</p>;
}

// React內
const result = Greeting(props); // <p>Hello</p>
複製代碼

可是若是Greeting是一個類,那麼 React 就須要使用new來實例化它,而後在實例上調用render方法:ide

// 你的代碼
class Greeting extends React.Component {
  render() {
    return <p>Hello</p>;
  }
}

// React內
const instance = new Greeting(props); // Greeting {}
const result = instance.render(); // <p>Hello</p>
複製代碼

在這兩種狀況下,React的目標是獲取渲染節點(本例中,是<p> Hello </ p>),但確切的步驟取決於Greeting的類型。

那麼React如何知道某個東西是class類型仍是function類型呢?

事實上,這篇文章更多的是關於JavaScript而不是關於React。 如何你好奇React爲什麼以某種方式運做,讓咱們一塊兒挖掘其中的原理。

這是一段漫長的探求之旅。這篇文章沒有太多關於React自己的信息,咱們將討論newthisclassarrow functionprototype__ proto__instanceof這些概念,以及這些東西如何在JavaScript中運做的機制。幸運的是,當你僅僅是使用React時,你不須要考慮這麼多。但你若是要深究React……

(若是你真的只想知道答案,請拉動到文章最後。)

爲何要用不一樣的調用方式?

首先,咱們須要理解以不一樣方式處理class和function的重要性。注意咱們在調用類時如何使用new運算符:

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

// If Greeting is a class
const instance = new Greeting(props); // Greeting {}
const result = instance.render(); // <p>Hello</p>
複製代碼

讓咱們大體的瞭解下new在Javascript中作了什麼:


在ES6以前,Javascript沒有class這個概念。可是,可使用純函數表現出和class類似的模式。 具體來講,你可使用new來調用相似類構造方法的函數,來表現出和class類似的模式

// 只是一個function
function Person(name) {
  this.name = name;
}

var fred = new Person('Fred'); // ✅ Person {name: 'Fred'}
var george = Person('George'); // 🔴 不會如期工做
//你今天仍然能夠寫這樣的代碼!在 `DevTools` 中嘗試一下。
複製代碼

若是不用new修飾Person('Fred'),Person內部的this在裏面會指向 window 或者 undefined 。結果就是代碼會崩潰或者像給window.name賦值同樣愚蠢。

在調用以前添加new,等於說:「嘿 JavaScript,我知道Person只是一個函數,但讓咱們僞裝它是個類構造函數。 建立一個{}對象 並在Person函數內將this指向該對象, 這樣我就能夠賦值像this.name這樣的東西。而後把那個對象返回給我。」

上面這些就是new操做符作的事情。

var fred = new Person('Fred'); // `Person`內,相同的對象做爲`this`
複製代碼

同時new操做符使上面的fred對象可使用Person.prototype上的任何內容。

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

var fred = new Person('Fred');
fred.sayHi();
複製代碼

這是以前人們在JavaScript模擬類的方式。

能夠看到的是在JavaScript早已有new。可是,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()那樣充當構造函數。忘記添加new會致使使人困惑的執行結果。

class語法讓咱們明確的告訴Javascript:「這不只僅是一個函數 - 它是一個類,它有一個構造函數」。 若是在調用class時忘記使用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.name被視爲window.name而不是george.name

可是,這意味着React須要在調用任何class以前使用new。它不能只是將其做爲普通函數直接調用,由於JavaScript會將其視爲錯誤!

class Counter extends React.Component {
  render() {
    return <p>Hello</p>;
  }
}

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

這意味着麻煩(麻煩就是在於React須要區分Class和Function……)。

探究React式如何解決的

babel之類編譯工具給解決問題帶來的麻煩

在咱們探究React式如何解決這個問題時,須要考慮到大多數人都使用Babel之類的編譯器來兼容瀏覽器(例如轉義class等),因此咱們須要在設計中考慮編譯器這種狀況。

在Babel的早期版本中,能夠在沒有new的狀況下調用類。可是經過生成一些額外的代碼,這個狀況已經被修復了:

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
複製代碼

你或許在打包文件中看到相似的代碼,這就是_classCallCheck函數所作的功能。 (您能夠經過設置「loose mode」不進行檢查來減少捆綁包大小,但這可能會使代碼最終轉換爲真正的原生類變得複雜。)

到如今爲止,你應該大體瞭解使用new與不使用new來調用某些內容之間的區別:

new Person() Person()
class this is a Person instance 🔴 TypeError
function this is a Person instance 😳 this is window or undefined

這之中的區別就是React爲何須要正確調用組件的重要緣由。 若是您的組件被定義爲類,React在調用它時須要使用new

那麼問題來了 React是否能夠判斷某個東西是否是一個class?

沒有那麼容易!即便咱們能夠在JavaScript es6 中區別class 和 function,這仍然不適用於像Babel這樣的工具處理以後的class。由於對於瀏覽器來講,它們只是單純的function而已(class被babel處理後)。


Okay,也許React能夠在每次調用時使用new?不幸的是,這也並不老是奏效。

異常狀況一:

做爲通常function,使用new調用它們會爲它們提供一個對象實例做爲this。對於做爲構造函數編寫的函數(如上面的Person),它是理想的,但它會給函數組件帶來混亂:

function Greeting() {
  // 咱們不但願「this」在這裏成爲任何一種狀況下的實例
  return <p>Hello</p>;
}
複製代碼

雖然這種狀況也是能夠容忍的,但還有另外兩個緣由能夠扼殺一直使用new的想法。

異常狀況二:

第一個是箭頭函數(未被babel編譯時)會使new調用失效,使用new調用箭頭函數會拋出一個異常

const Greeting = () => <p>Hello</p>;
new Greeting(); // 🔴 Greeting is not a constructor
複製代碼

這種狀況時是有意的,而且遵循箭頭函數的設計。箭頭函數的主要優勢之一是它們沒有本身的this綁定 - 取而代之的是 this被綁定到最近的函數體中。

class Friends extends React.Component {
  render() {
    const friends = this.props.friends;
    return friends.map(friend =>
      <Friend
        // `this` is resolved from the `render` method
        size={this.props.size}
        name={friend.name}
        key={friend.id}
      />
    );
  }
}
複製代碼

Okay,因此箭頭功能沒有本身的this 這意味着箭頭函數沒法成爲構造者!

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

所以,JavaScript不容許使用new調用箭頭函數。若是你這樣作,只會產生錯誤。這相似於JavaScript不容許在沒有new的狀況下調用類的方式。

這很不錯,但它也使咱們在所有函數調用前添加new的計劃失敗。 React不能夠在全部狀況下調用new,由於它會破壞箭頭函數!咱們能夠嘗試經過缺乏prototype來判斷出箭頭函數:

(() => {}).prototype // undefined
(function() {}).prototype // {constructor: f}
複製代碼

可是這個不適用於使用babel編譯的函數。這可能不是什麼大問題,但還有另外一個緣由讓這種方法失敗。

異常狀況三:

咱們不能老是使用new的另外一個緣由是,這樣作不支持返回字符串或其餘原始類型。

function Greeting() {
  return 'Hello';
}

Greeting(); // ✅ 'Hello'
new Greeting(); // 😳 Greeting {}
複製代碼

這再次與new的設計奇怪表現有關。正如咱們以前看到的那樣,new告訴JavaScript引擎建立一個對象,在函數內部建立該對象,而後將該對象做爲new的結果。 可是,JavaScript還容許使用new調用的函數經過返回一些其餘對象來覆蓋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會徹底忽略函數的返回值。若是你返回一個字符串或一個數字,就好像沒有返回同樣。

function Answer() {
  return 42;
}

Answer(); // ✅ 42
new Answer(); // 😳 Answer {}
複製代碼

當使用new調用函數時,沒法從函數中讀取原始返回值(如數字或字符串)。所以,若是React老是使用new,它將沒法支持返回字符串類型的函數(組件)

這是不可接受的,因此咱們得另尋他法。

解決方式

到目前爲止咱們瞭解到了什麼? React須要用new調用類(兼容Babel狀況),但它須要調用常規函數或箭頭函數(兼容Babel)時不能使用new。同時並無可靠的方法來區分它們。 若是咱們沒法解決一個廣泛問題,那麼咱們能解決一個更具體的問題嗎?

將Component定義爲class時,你可能但願繼承React.Component使用其內置方法(好比this.setState())。那麼咱們能夠只檢測React.Component子類,而不是嘗試檢測全部class嗎?

劇透:這正是React所作的。

prototype__proto__

也許,判斷Greeting是不是React component class的通常方法是測試Greeting.prototype instanceof React.Component

class A {}
class B extends A {}

console.log(B.prototype instanceof A); // true
複製代碼

我知道你在想什麼。剛剛發生了什麼?!要回答這個問題,咱們須要瞭解JavaScript原型。

你可能熟悉原型鏈。JavaScript中的每一個對象均可能有一個「原型」。當咱們編寫fred.sayHi()fred對象沒有sayHi屬性時,咱們在fred的原型上查找sayHi屬性。若是咱們在那裏找不到它,咱們會看看鏈中的下一個原型--fred的原型的原型。並以此類推。

使人困惑的是,類或函數的prototype屬性並不指向該值的原型。 我不是在開玩笑。

function Person() {}

console.log(Person.prototype); // 🤪 Not Person's prototype
console.log(Person.__proto__); // 😳 Person's prototype
複製代碼

因此「原型鏈」更像是__proto __.__ proto __.__ proto__而不是prototype.prototype.prototype。我花了許多年才瞭解到這一點。

全部對象的 __proto__ 都指向其構造器的prototype 函數或類的prototype屬性就是這樣一個東西

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__,最終它被勉強標準化。

至今我仍然以爲「prototype的屬性沒有給你一個值的原型「很是使人困惑(例如,fred.prototype未定義,由於fred不是一個函數)。就我的而言,我認爲這是致使經驗豐富的開發人員也會誤解JavaScript原型的最大緣由。

extends 與 原型鏈

這帖子有點長 不是嗎?別放棄!如今已經講了80%的內容了,讓咱們繼續吧

咱們知道,當說調用obj.foo時,JavaScript實際上在objobj .__ proto__obj .__ proto __.__ proto__中尋找foo,依此類推。

對於類來講原型鏈機制會更加複雜,但extends會使類完美適用原型鏈機制。這也是React類的實例訪問setState之類的方法的原理:

class Greeting extends React.Component {
  render() {
    return <p>Hello</p>;
  }
}

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)
複製代碼

換句話說,類實例的__protp__鏈會鏡像拷貝類的繼承關係:

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

// `__proto__` chain
new Greeting()
  → Greeting.prototype
    → React.Component.prototype
      → Object.prototype
複製代碼

如此兩個鏈(繼承鏈 原型鏈)

instanceof 判斷方式

因爲__proto__鏈鏡像拷貝類的繼承關係,所以咱們能夠經過Greeting的原型鏈來判斷Greeting是否繼承了React.Component

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

方便的是,x instanceof Y就是相同的搜索原理。它在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組件類仍是通常函數。

React 判斷方式

但這並非React所作的。 😳

instanceof解決方案的一個隱患是:當頁面上有多個React副本時,咱們正在檢查的組件可能繼承自另外一個React副本的React.Component,這種instanceof方式就會失效。 在一個項目中混合使用React的多個副本是很差的方式,但咱們應該儘量避免出現因爲歷史遺留所產生的這種問題。 (使用Hooks,咱們可能須要強制刪除重複數據。)

另外一種可能的騷操做是檢查原型上是否存在render方法。可是,當時還不清楚組件API將如何變換。每一個判斷操做都有成本咱們不想添加多於一次的操做。若是將render定義爲實例方法(例如使用類屬性語法),這也不起做用。

所以,React爲基本組件添加了一個特殊標誌。React經過檢查是該標誌來判斷一個東西是不是React組件類。

最初該標誌在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
複製代碼

這就是React如何判斷class的所有內容。


現在在React中使用就是isReactComponent標誌檢查。

若是不擴展React.Component,React將不會在原型上找到isReactComponent,也不會將組件視爲類。如今你知道爲何Cannot call a class as a function問題最受歡迎的回答是添加extends React.Component。最後,添加了一個prototype.render存在時,但prototype.isReactComponent不存在的警告

實際的解決方案很是簡單,但我用大量的時間解釋了爲何React最終採起這個解決方案,以及替代方案是什麼。 你可能以爲博文這個解釋過程有點囉嗦,

根據個人經驗,開發庫API常常會遇到這種狀況。爲了使API易於使用,開發者須要考慮語言語義(可能,對於多種語言,包括將來的方向)、運行時性能、是否編譯狀況的兼容、完總體系和打包解決方案的狀態、 早期預警和許多其餘事情。 最終結果可能並不優雅,但必須實用。

若是最終API是成功的,則用戶永遠沒必要考慮此過程。 取而代之的是他們只須要專一於建立應用程序。

但若是你也好奇......去探究其中的緣由仍是十分有趣的。

相關文章
相關標籤/搜索