原文 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 |
✅this 是Person 實例 |
🔴TypeError |
function |
✅this 是Person 實例 |
😳this 指向window 或undefined |
這就是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實際上去尋找obj
的foo
,找不到再去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。
可是若是你很好奇,能幫助你理解它如何工做也很棒。