React 是如果分辨函式式元件和類元件的?
原文連結: ofollow,noindex">How Does React Tell a Class from a Function?
本文中通過探討這個問題,涉及到了JavaScript/">JavaScript中大量的重要概念像原型、原型鏈、this、類、繼承等,通過思考這個問題對這些知識進行一個回顧,不失為一個好的學習方法,但如果你只是想知道這個問題的答案,就像作者說的那樣,直接滾動到底部吧。
限於本人水平有限,翻譯不到位的地方,敬請諒解。
正文
在React中我們可以用Function定義一個元件:
function Greeting() { return <p>Hello</p>; } 複製程式碼
同樣可以使用Class定義一個元件:
class Greeting extends React.Component { render() { return <p>Hello</p>; } } 複製程式碼
在React推出Hooks之前,Class定義的元件是使用像state這樣的功能的唯一方式。
當你想渲染的時候,你不需要關心它是怎樣定義的:
// Class or function — whatever. <Greeting /> 複製程式碼
但是React會關心這些不同。
如果 Greeting
是一個函式,React需要像下面這樣呼叫:
// Your code function Greeting() { return <p>Hello</p>; } // Inside React const result = Greeting(props); // <p>Hello</p> 複製程式碼
但是如果 Greeting
是一個類,React需要用 new
命令建立一個例項,然後呼叫建立的例項的 render
方法:
// Your code class Greeting extends React.Component { render() { return <p>Hello</p>; } } // Inside React const instance = new Greeting(props); // Greeting {} const result = instance.render(); // <p>Hello</p> 複製程式碼
那麼React是怎麼分辨 class
或者 function
的呢?
這會是一個比較長的探索之旅,這篇文章不會過多的討論React,我們將探索 new,this,class,箭頭函式,prototype,__proto__,instanceof
的某些方面以及它們是怎麼在JavaScript中一起工作的。
首先,我們需要理解為什麼區分functions和class之間不同是如此重要,注意怎樣使用 new
命令去呼叫一個class:
// 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中究竟是幹什麼的。
之前JavaScript並沒有Class,但是你能用一個正常的函式去模擬Class。 具體地說,你可以使用任何通過new呼叫的函式去模擬class的建構函式 :
// Just a function function Person(name) { this.name = name; } var fred = new Person('Fred'); // :white_check_mark: Person {name: 'Fred'} var george = Person('George'); // :red_circle: Won’t work 複製程式碼
現在你仍然可以這樣寫,麻溜試一下喲。
如果你不用new命令呼叫 Person('Fred')
,函式中this會指向 window
或者 undefined
,這樣我們的程式碼將會炸掉或者出現怪異的行為像設定了 window.name
。
通過使用new命令呼叫函式,相當於我們說:“JavaScript,你好,我知道 Person
僅僅只是一個普通函式但是讓我們假設它就是類的一個建構函式。建立一個 {}
物件然後傳入 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新增Class(類)之前是怎樣模擬Class(類)的。
如果你定義了一個函式,JavaScript是不能確定你會像 alert()
一樣直接呼叫或者作為一個建構函式像 new Person()
。忘了使用new命令去呼叫像 Person
這樣的函式將會導致一些令人困惑的行為。
Class(類)的語法相當於告訴我們:“這不僅僅是一個函式,它是一個有建構函式的類”。如果你在呼叫Class(類)的時候,忘了加new命令,JavaScript將會丟擲一個錯誤:
et fred = new Person('Fred'); // :white_check_mark:If Person is a function: works fine // :white_check_mark:If Person is a class: works fine too let george = Person('George'); // We forgot `new` // :flushed: If Person is a constructor-like function: confusing behavior // :red_circle: If Person is a class: fails immediately 複製程式碼
這將幫助我們及早的發現錯誤,而不是等到出現明顯的bug的時候才知道,像 this.name
變成了 window.name
而不是 george.name
。
不管怎樣,這意味著React需要使用new命令去呼叫所有的類,它不能像呼叫正常函式一樣去呼叫類,如果這樣做了,JavaScript會報錯的!
class Counter extends React.Component { render() { return <p>Hello</p>; } } // :red_circle: React can't just do this: const instance = Counter(props); 複製程式碼
上面是錯誤的寫法。
在我們講React是怎麼解決的之前,我們要知道大多數人會使用Babel去編譯React專案,目的是為了讓專案中使用的最新特性像class(類)能夠相容低端的瀏覽器,這樣我們就需要了解的Babel的編譯機制。
在Babel早期的版本中,class(類)可以使用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'); // :white_check_mark: Okay Person('George');// :red_circle: Can’t call class as a function 複製程式碼
你可能有在打包出來的檔案中看到過上面的程式碼,這就是 _classCallCheck
所做的事情。
到目前為止,你應該已經大概掌握了使用new命令和不使用new命令之間的差別:

這就是為什麼React需要正確呼叫元件是如此重要的原因。如果你使用class(類)定義一個元件,React需要使用new命令去呼叫。
那麼React能判斷出一個元件是否是由class(類)定義的呢?
沒那麼容易,即使我們能分辨出函式和class(類):
function isClass(func) { return typeof func === 'function' && /^class\s/.test(Function.prototype.toString.call(func)); } 複製程式碼
但如果我們使用了像Babel這樣的編譯工具,上面的方法是不會起作用的,Babel會將class(類)編譯為:
// 類 class Person { } // Babel編譯後 "use strict"; function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } var Person = function Person() { _classCallCheck(this, Person); }; 複製程式碼
對於瀏覽器來說,它們都是普通的函式。
ok,React裡面的函式能不能都使用new命令呼叫呢?答案是不能。
用new命令呼叫普通函式的時候,會傳入一個物件例項作為 this
,像上面的 Person
那樣將函式作為建構函式來使用是可以的,但是對於函式式的元件卻會讓人懵逼的:
function Greeting() { // We wouldn’t expect `this` to be any kind of instance here return <p>Hello</p>; } 複製程式碼
即使你能這樣寫,下面的兩個原因會杜絕你的這種想法。
第一個原因:使用new命令呼叫箭頭函式(未經Babel編譯過)會報錯
const Greeting = () => <p>Hello</p>; new Greeting(); // :red_circle: 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} /> ); } } 複製程式碼
Tips:
如果不太理解的童鞋,可以參考下面的文章
ok,箭頭函式沒有自己的 this
,這就意味著它不能作為建構函式:
const Person = (name) => { // :red_circle: This wouldn’t make sense! this.name = name; } 複製程式碼
因此,JavaScript不能使用new命令呼叫箭頭函式,如果你這樣做了,程式就會報錯,和你不用new命令去呼叫class(類)一樣。
這是非常好的,但是不利於我們的計劃,因為箭頭函式的存在,React不能只用new命令去呼叫,當然我們也能試著去通過箭頭函式沒有 prototype
去區分它們,然後不用new命令呼叫:
(() => {}).prototype // undefined (function() {}).prototype // {constructor: f} 複製程式碼
但是如果你的專案中使用了Babel,這也不是個好主意,還有另一個原因使這條路徹底走不通。
這個原因是使用new命令呼叫React中的函式式元件,會獲取不到這些函式式元件返回的字串或者其他基本資料型別。
function Greeting() { return 'Hello'; } Greeting(); // :white_check_mark: 'Hello' new Greeting(); // :flushed: Greeting {} 複製程式碼
關於這點,我們需要知道new命令到底幹了什麼?
通過new操作符呼叫建構函式,會經歷以下4個階段
- 建立一個新的物件;
- 將建構函式的this指向這個新物件;
- 指向建構函式的程式碼,為這個物件新增屬性,方法等;
- 返回新物件。
關於這些內容在 全方位解讀this-這波能反殺 有更為詳細的解釋。
如果React只使用new命令呼叫函式或者類,那麼就無法支援返回字串或者其他原始資料型別的元件,這肯定是不能接受的。
到目前為止,我們知道了,React需要去使用new命令呼叫class(包括經過Babel編譯的),不使用new命令呼叫正常函式和箭頭函式,這仍沒有一個可行的方法去區分它們。
當你使用class(類)宣告一個元件,你肯定想繼承 React.Component
中像 this.setState()
一樣的內部方法。與其去費力去分辨一個函式是不是一個類,還不如我們去驗證這個類是不是 React.Component
的例項。
劇透:React就是這麼做的。
可能我們常用的檢測 Greeting
是React元件示例的方法是 Greeting.prototype instanceof React.Component
:
class A {} class B extends A {} console.log(B.prototype instanceof A); // true 複製程式碼
我猜你估計在想,這中間發生了什麼?為了回答這個問題,我們需要理解JavaScript的原型。
你可能已經非常熟悉原型鏈了,JavaScript中每一個物件都有一個“prototype(原型)”。
下面的示例和圖來源於 前端基礎進階(九):詳解面向物件、建構函式、原型與原型鏈 ,個人覺得比原文示例更能說明問題
// 宣告建構函式 function Person(name, age) { this.name = name; this.age = age; } // 通過prototye屬性,將方法掛載到原型物件上 Person.prototype.getName = function() { return this.name; } var p1 = new Person('tim', 10); var p2 = new Person('jak', 22); console.log(p1.getName === p2.getName); // true 複製程式碼

當我們想要呼叫p1上的getName方法時,但是p1自身並沒有這個方法,它會在p1的原型上尋找,如果沒有找到我們會沿著原型鏈在上一層的原型上繼續找,也就是在p1的原型的原型...,一直找下去,直到原型鏈的終極 null 。
原型鏈更像 __proto__.__proto__.__proto__
而不是 prototype.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__
。如果你想往原型上新增一些東西,你應該新增到 Person.prototype
上,那新增到 __proto___
可以嗎?當然可以,能生效,但是這樣不符合規範的,有效能問題和相容性問題,詳情點選這裡。
早期的瀏覽器是沒有暴露 __proto
屬性的,因為原型類是一個內部的概念,後來一些瀏覽器逐漸支援,在ECMAScript2015規範中被標準化了,想要獲取某個物件的原型,建議老老實實的使用 Object.getPrototypeOf()
。
我們現在已經知道了,當訪問 obj.foo
的時候,JavaScript通常在 obj
中這樣尋找 foo
, obj.__proto__,obj.__proto__.__proto__
...
定義一個類元件,你可能看不到原型鏈這套機制,但是 extends(繼承)
只是原型鏈的語法糖,React的類元件就是這樣訪問到 React.Component
中像 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) 複製程式碼
換句話說, 當你使用類的時候,一個例項的原型鏈對映這個類的層級
// `extends` chain Greeting → React.Component → Object (implicitly) // `__proto__` chain new Greeting() → Greeting.prototype → React.Component.prototype → Object.prototype 複製程式碼
因為原型鏈對映類的層級,那我們就能從一個繼承自 React.Component
的元件 Greeting
的 Greeting.prototype
開始,順著原型鏈往下找:
// `__proto__` chain new Greeting() → Greeting.prototype //️ We start here → React.Component.prototype // :white_check_mark: Found it! → Object.prototype 複製程式碼
實際上, x instanceof y
就是做的這種查詢,它沿著x的原型鏈查詢y的原型。
通常這用來確定某個例項是否是一個類的例項:
let greeting = new Greeting(); console.log(greeting instanceof Greeting); // true // greeting (️ We start here) //.__proto__ → Greeting.prototype (:white_check_mark: 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 (:white_check_mark: 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 (:white_check_mark: Found it!) console.log(greeting instanceof Banana); // false // greeting (️ We start here) //.__proto__ → Greeting.prototype //.__proto__ → React.Component.prototype //.__proto__ → Object.prototype (:no_good: Did not find it!) 複製程式碼
並且它也能用來檢測一個類是否繼承自另一個類:
console.log(Greeting.prototype instanceof React.Component); // greeting //.__proto__ → Greeting.prototype (️ We start here) //.__proto__ → React.Component.prototype (:white_check_mark: Found it!) //.__proto__ → Object.prototype 複製程式碼
我們就能通過這種方式檢測出一個元件是函式元件還是類元件。
然而React並沒有這樣做。
作者此處還探討了兩種方案,在此略去,有興趣看原文喲。
實際上React對基礎的元件也就是 React.Component
添加了一個標記,並通過這個標記來區分一個元件是否是一個類元件。
// Inside React class Component {} Component.isReactClass = {}; // We can check it like this class Greeting extends Component {} console.log(Greeting.isReactClass); // :white_check_mark: Yes 複製程式碼
像上面這樣把標記直接新增到基礎元件自身,有時候會出現 靜態屬性丟失 的情況,所以我們應該把標記新增到 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); // :white_check_mark: Yes 複製程式碼
React就是這樣解決的。
後面還有幾段,參考文末另一位大兄弟的譯文吧。
後續
這文章有點長,涉及的知識點也比較多,最後的解決方案,看似挺簡單的,實際上走到這一步並不簡單,希望大家都有所收穫。 翻譯到一半的時候,在React的一個Issues中發現另一個人這篇文章的譯文,有興趣的童鞋, 可以點選閱讀 。