React怎樣從函式中辨別類
考慮用函式定義的元件Greeting
:
function Greeting() { return <p>Hello</p> } 複製程式碼
React也支援使用類定義它:
class Greeting extends React.Component { render() { return <p>Hello</p> } } 複製程式碼
(直到最近,那是唯一的方式使用state特性)
當你render一個<Greeting />
的時候,你不需要關心它是如何定義的。
// 類 或 函式 <Greeting /> 複製程式碼
但是React自己關心它們的不同!
如果Greeting
是一個函式,React需要呼叫它:
// 你的程式碼 function Greeting() { return <p>Hello</p> } // React 內部 const result = Greeting(props); // <p>Hello</p> 複製程式碼
但是如果Greeting
是一個類,React需要通過new
操作符將它例項化,然後呼叫例項的render
方法:
// 你的程式碼 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是怎樣分辨類和函式的呢?
正如我之前文章所說的,不知道這些你也能使用React生產。多年來我都不知道這件事。請不要把這個問題變成面試問題。事實上,這篇文章更多的關於是JavaScript/">JavaScript而不是React。
這篇文章是為那些好奇於React為何會在一種特定方式下工作的讀者寫的。你是那樣的讀者嗎?讓我們一起深入探討吧。
這是一段較長的旅程。這篇部落格不會有很多關於React的資訊,但是我們會審查某些方面new
,this
,class
,arrow functions
,prototype
,__proto__
,instanceof
,和這些東西在JavaScript
中是如何一起工作的。幸運的是,當你使用React時你不需要考慮太多這方面的問題。如果你正在實現那麼...
(如果你只是想要知道結果,請導航到最底部。)
首先,我們需要了解為什麼將函式和類視為不同的是重要的。注意我們怎樣使用new操作符當呼叫一個類時:
// 如果Greeting是一個函式 const result = Greeting(props); // <p>Hello</p> // 如果Greeting是一個類 const instance = new Greeting(props); // Greeting {} const result = instance.render(); // <p>Hello</p> 複製程式碼
讓我們粗略瞭解一下JavaScript中的new操作符是做什麼的。
在過去,JavaScript中不存在類。可是,你可以通過簡單的函式表示一個相似的類。具體說來,你可以使用任何函式類似於類的角色通過新增new
在它被呼叫時:
// 僅僅是個函式 function Person(name) { this.name = name; } var fred = new Person('Fred'); // Person {name: 'Fred'} var george = Person('George'); // Won't work 複製程式碼
你現在依然能夠這樣寫!嘗試寫在DevTools上。
如果你呼叫Person('Fred')
沒有new
,它內部的this
將指向全域性或者不可用(例如,window
或者undefined
).因此我們的程式碼可能崩潰或者做一些愚蠢的事情例如設定window.name
。
呼叫之前通過新增new
,我們說:“嗨JavaScript
,我知道Person
是一個函式但是假裝它是一個類構造器。
建立一個{}
物件並將Person
函式內部的this
指向該物件,因此我可以定義一些東西比如this.name
。然後它會返回一個物件給我。
這就是new
操作符所做的事情。
var fred = new Person('Fred'); // 相同的物件this在Person內部 複製程式碼
new
操作符也能使我們定義在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
已經存在一段時間了。然後,classe
沒出現多久。這讓我們重寫上面的程式碼,以便更加地匹配我們的意圖:
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
特殊的處理像Person
這樣的函式會導致混亂的行為。
Class
語法讓我們說:“這不只是函式——它是一個類並且擁有構造器”。如果你忘記使用new
當呼叫它的時候,JavaScript會丟擲一個錯誤:
let fred = new Person('Fred'); // 如果Person是一個函式:正常工作 // 如果Person是一個類:也能正常工作 let george = Person('George'); // 沒有加new // 如果Person是一個建構函式的樣子,迷惑的行為 // 如果Person是一個類:將立即失敗 複製程式碼
這會幫助我們很早地捕獲錯誤而不是等到一些迷惑的bug
出現就像this.name
被認為是window.name
而不是george.name
。
然而,這就意味著React需要將new
加上在呼叫任何類之前。不能僅僅將它作為普通的函式呼叫,否則JavaScript會將它看做是一個錯誤!
class Counter extends React.Component { render() { return <p>Hello</p> } } // React 不能這樣做 const instance = Counter(props); 複製程式碼
這樣會導致問題。
在我們看React是如何解決這個問題時,記住大多數人在使用React時,為了相容老的瀏覽器,都會使用編譯器比如Babel
將現有的一些特性例如類進行編譯是重要的。因此我們需要考慮編譯因素在我們的設計中。
在最近的Babel
版本中,類可以不使用new
而被呼叫。但是,這很快被修復了——通過生成一些額外的程式碼:
function Person(name) { // 大大的精簡了從Babel的輸出中 if (!(this instanceof Person)) { throw new TypeError("Cannot call a class as a function"); } // 我們的程式碼 this.name = name; } new Person('Fred'); // Okay Person('George'); // 不能呼叫類像函式一樣 複製程式碼
你可能有一些程式碼像這樣在你的包中。那些都是_classCallCheck
函式所做的事情。(你可以通過選擇“鬆散模式”來減少包的大小,而無需進行檢查,但是這可能會使你最終轉換到真正的本地類的過程變得複雜。)
到目前為止,你應該大致瞭解了使用new或不使用new呼叫某些東西的區別:
New Person() Person()
class this是一個Person例項 TypeError
function this是一個Person例項 this是window或者undefined
這就是為什麼正確呼叫元件的React非常重要。
如果你的元件被定義為一個類,則React在呼叫它時需要使用new
。
那麼React僅僅檢查某個東西是否是類嗎?
不會這麼簡單的!儘管我們ofollow,noindex">在JavaScript中能夠從函式中分辨出類 ,這仍然不適用於Babel等工具處理的類。對於瀏覽器來說,它們只是普通函式。對React是不好的。
好吧,也許React在每次呼叫時都要使用new
?不幸的是,這也不總是有效的。
對於常規函式,用new
呼叫它們會給它們一個this
的物件例項。對於作為建構函式編寫的函式(如我們上面提到的Person)來說,它是可取的,但是對於函式元件來說,它可能會令人混淆:
function Greeting() { // 我們不期望this是任何型別的例項 return <p>Hello</p> } 複製程式碼
不過,這是可以容忍的。有兩個其他的原因扼殺了這個想法。
總是使用`new`不起作用的第一個原因是本地箭頭函式(不是`Babel`編譯的那些函式)),呼叫使用`new`會丟擲一個錯誤:
const Greeting = () => <p>Hello</p>; new Greeting(); // Greeting不是一個構造器 複製程式碼
這種行為是有意的,並遵循箭頭函式的設計。箭頭函式的一個主要好處是它們沒有自己的this
值——相反,this
的值指向最近的常規函式:
class Friends extends React.Component { render() { const friends = this.props.friends; return friends.map(friend => <Friend // `this`是render中的值 size={this.props.size} name={friend.name} key={friend.id} /> ); } } 複製程式碼
箭頭函式沒有自己的this
。這意味著它們作為建構函式將完全無用!
const Person = (name) => { // :red_circle: 這樣是沒有意義的 this.name = name; } 複製程式碼
因此,
JavaScript不允許使用new
呼叫箭頭函式
。如果你這樣做了,無論如何你都可能犯了一個錯誤,最好早點告訴你。這類似於JavaScript在沒有new
的情況下不允許呼叫類。
這很好,但也破壞了我們的計劃。React不能對所有東西都呼叫new
,因為它會破壞箭頭函式!我們可以通過箭頭函式缺少原型來檢測它們,而不僅僅new
一個:
(() => {}).prototype // undefined (function() {}).prototype // {constructor: f} 複製程式碼
但這不適用 於Babel編譯的函式。這可能不是什麼大問題,但還有一個原因使這種方法成為死衚衕。
我們不能總是使用new的另一個原因是,它將阻止對返回字串或其他基本型別的元件的支援。
function Greeting() { return 'Hello'; } Greeting(); // :white_check_mark: 'Hello' new Greeting(); // :flushed: Greeting {} 複製程式碼
這又一次與new操作符設計的怪癖有關。正如我們前面看到的,new
告訴JavaScript引擎建立一個物件,將該物件置於函式內部,然後將該物件作為new
的結果提供給我們。
然而,JavaScript還允許一個用new呼叫的函式通過返回一些其他物件來覆蓋new
的返回值。據推測,這對於我們希望重用例項的池等模式是有用的:
var zeroVector = null; function Vector(x, y) { if (x === 0 && y === 0) { if (zeroVector !== null) { 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); // :astonished: b === c 複製程式碼
然而,new
也會完全忽視函式返回非物件的值。如果你返回一個字串或者數字,就像沒有返回一樣。
function Answer() { return 42; } Answer(); // :white_check_mark: 42 new Answer(); // :flushed: Answer {} 複製程式碼
在使用new
呼叫函式時,無法從函式讀取原始返回值(如數字或字串)。因此,如果React總是使用new
,將不能支援返回字串的元件!
這是不能接受的,所以我們需要妥協。
到目前為止,我們都學到了些什麼?React在呼叫classe
時(包括使用Babel編譯後的結果)需要使用new
,而一般的函式或者箭頭函式(包括使用Babel
編譯後的結果)被呼叫時不需要使用new
。並且沒有一種可靠的方式能分辨出它們的區別。
如果我們不能解決一般的問題,還能解決特殊的問題嗎?
當你使用類定義一個元件時,你可能希望通過繼承React.Component
來擴充套件一些方法,如this.setState()
。與其檢測所有類,不如只檢測React.Component
的後代。
劇透:這就是React所做的事情。
也許,檢測Greeting
是React Component
型別慣用的方式是通過檢測Greeting.prototype instanceof React.Component
:
class A {} class B extends A {} console.log(B.prototype instanceof A); // true 複製程式碼
我知道你在想什麼。剛剛發生了什麼?!為了回答這個問題,我們需要先了解JavaScript中的prototype
。
你可能熟悉原型鏈。JavaScript中的每個物件都有一個“prototype”。當我們寫fred.sayHi()
但是fred
物件沒有sayHi
屬性,我們將在它的原型上查詢sayHi
。如果我們沒有在那裡找到,我們將繼續沿著原型鏈查詢——fred
的prototype
的prototype
。等等。
疑惑的是,類或函式的prototype
屬性不指向該值的原型。我沒有在開玩笑。
function Person() {} console.log(Person.prototype); //Not Person's prototype console.log(Person.__proto__); // :flushed: Person's prototype 複製程式碼
因此原型鏈更像是__proto__.__proto__.__proto__
而不是prototype.prototype.prototype
。這花了我好幾年才知道。(存疑???)
那麼函式或類的原型屬性是什麼呢?它是__proto__
,用於類或函式的所有新物件!
function Person(name) { this.name = name; } Person.prototype.sayHi = function() { alert('Hi, I am ' + this.name); } var fred = new Person('Fred'); // 設定‘fred.__proto__’為‘Person.prototype’ 複製程式碼
__proto__
鏈就是JavaScript如何查詢屬性的方法。
fred.sayHi(); // 1. fred有sayHi屬性嗎?沒有 // 2. fred.__proto__有一個sayHi屬性嗎?是的,Call it! fred.toString(); // 1. fred有toString屬性嗎?沒有 // 2. fred.__proto__有一個toString屬性嗎?沒有! // 3. fred.__proto__.__proto__有一個toString屬性嗎?是的,Call it! 複製程式碼
在實踐中,除非除錯與原型鏈相關的內容,否則幾乎不需要直接從程式碼中接觸__proto__
。如果你想使一些東西在fred.__proto__
起作用,你應該將它寫在Person.prototype
上。至少它最初是這樣設計的。
__proto__
屬性一開始甚至不應該由瀏覽器公開,因為原型鏈被認為是一個內部概念。但是一些瀏覽器添加了__proto__
,最終勉強實現了標準化(但是反對使用Object.getPrototypeOf()
)。
但是我仍然覺得很困惑,一個叫做prototype
的屬性並沒有給你一個值的原型(例如,fred.prototype
是undefined
,因為fred
不是一個函式)。個人看來,我認為這是即使有經驗的開發人員也容易誤解JavaScript原型的最大原因。
這是一篇很長的文章,嗯哼?我已經說了80%
的東西了。繼續。
我們知道當說obj.foo,JavaScript
真的在obj
,obj.__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) 複製程式碼
換句話說,當你使用類時,例項的__proto__
鏈“對映”了類的層次結構:
// `extends` chain Greeting → React.Component → Object (implicitly) // `__proto__` chain new Greeting() → Greeting.prototype → React.Component.prototype → Object.prototype 複製程式碼
2種鏈式
因為__proto__
鏈反映了類的層次結構,我們可以根據Greeting.prototype
檢查Greeting
是否繼承自React.Component
,然後跟著__proto__
鏈找下去:
// `__proto__` chain new Greeting() → Greeting.prototype //從這裡開始 → React.Component.prototype // :white_check_mark: 找到了 → 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 (: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的功能。 :flushed:
需要注意的是當頁面中存在多個React的副本時,instanceof
方法是沒有用的,並且我們檢查的元件繼承自另一個React拷貝的React.Component
。將多個React副本混合在一個專案中是不好的,原因有幾個,但在歷史上,我們總是儘可能避免出現問題。(但是,使用鉤子,我們可能需要
強制刪除重複資料。)
另一種具有啟發性的方法是檢查原型上是否存在render方法。但是,那時候還不清楚
元件API中將包括哪些東西。每一種檢查都會增加消耗因此我們不想增加多於一個的檢查方式。如果在例項上新增render
方法,這也不會工作的,例如使用類屬性語法。
因此取而代之的是,React在基礎元件上新增 了一個特殊的標誌,React檢查標誌是否存在,這就是它能辨別某些東西是否是React元件類。
原本標誌是設於基礎React.Component
類自身上面的:
// React 內部 class Component {} Component.isReactClass = {}; // 我們可以這樣檢查它 class Greeting extends Component {} console.log(Greeting.isReactClass) ; // yes 複製程式碼
但是,我們對於一些類的實現目標是不想
要複製靜態屬性(或者設定不標準的__proto__
), 因此標誌消失了。
這也是為什麼React將標誌移入
到React.Component.prototype
的原因:
// React 內部 class Component {} Component.prototype.isReactComponent = {}; // 我們能夠這樣檢查它 class Greeting extends Component {} console.log(Greeting.prototype.isReactComponent); // yes 複製程式碼
這就是它的全部。
你也許會好奇它為何是一個物件而不是一個boolean
值。這在實踐中並沒有多大影響但是在jest
早期版本(在Jest
是Good™
️之前)中會預設的自動模擬。生成的mocks省略了基本屬性,破壞了檢查
。感謝你,Jest.
近來isReactComponent
檢查被用在React中
。
如果你沒有繼承React.Component
,React不會在原型中尋找isReactComponent
,也不會將元件視為一個類。現在你知道為什麼獲得最高票的問題“不能呼叫類作為一個函式”錯誤的回答是“extends React.Component
”.最後,當prototype.render
存在而prototype.isReactComponent
不存在時會出現一個警告
。
你也許會說這邊文章有點誘導轉向法的感覺。真正的解決方案很簡單,但是我偏題的解釋了大一堆而以這種方法結束,有什麼可供選擇呢
在我的經驗中,庫api
通常就是這種情況。要使API
易於使用,通常需要考慮語言語義(可能是幾種語言,包括未來的發展方向),執行時效能,有無編譯時間步態的人類工效學,生態系統的狀態和打包解決方案,早期警告,以及許多其他東西。最終的結果不一定是最優雅的,但必須是可實踐的。
**如果最後API
成功了,使用者絕不會考慮它的過程。**相反他們會剛專注於建立APPs
。
但是如果你也好奇,知道它是如何工作是很美妙的一件事情。
原文連結:overreacted.io/how-does-re… byDan Abreamov