本文中經過探討這個問題,涉及到了JavaScript中大量的重要概念像原型、原型鏈、this、類、繼承等,經過思考這個問題對這些知識進行一個回顧,不失爲一個好的學習方法,但若是你只是想知道這個問題的答案,就像做者說的那樣,直接滾動到底部吧。前端
限於本人水平有限,翻譯不到位的地方,敬請諒解。react
在React中咱們能夠用函數定義一個組件:git
function Greeting() {
return <p>Hello</p>;
}
複製代碼
一樣可使用Class定義一個組件:es6
class Greeting extends React.Component {
render() {
return <p>Hello</p>;
}
}
複製代碼
在React推出Hooks以前,Class定義的組件是使用像state這樣的功能的惟一方式。github
當你想渲染的時候,你不須要關心它是怎樣定義的:數組
// Class or function — whatever.
<Greeting />
複製代碼
可是React會關心這些不一樣。瀏覽器
若是Greeting
是一個函數,React須要像下面這樣調用:bash
// Your code
function Greeting() {
return <p>Hello</p>;
}
// Inside React
const result = Greeting(props); // <p>Hello</p>
複製代碼
可是若是Greeting
是一個類,React須要用new
命令建立一個實例,而後調用建立的實例的render
方法:ide
// 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'); // ✅ Person {name: 'Fred'}
var george = Person('George'); // 🔴 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');
// ✅ 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
複製代碼
這將幫助咱們及早的發現錯誤,而不是等到出現明顯的bug的時候才知道,像this.name
變成了window.name
而不是george.name
。
無論怎樣,這意味着React須要使用new命令去調用全部的類,它不能像調用正常函數同樣去調用類,若是這樣作了,JavaScript會報錯的!
class Counter extends React.Component {
render() {
return <p>Hello</p>;
}
}
// 🔴 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'); // ✅ Okay
Person('George'); // 🔴 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(); // 🔴 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}
/>
);
}
}
複製代碼
若是不太理解的童鞋,能夠參考下面的文章
ok,箭頭函數沒有本身的this
,這就意味着它不能做爲構造函數:
const Person = (name) => {
// 🔴 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(); // ✅ 'Hello'
new Greeting(); // 😳 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 // ✅ 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 (✅ 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.Component
添加了一個標記,並經過這個標記來區分一個組件是不是一個類組件。
// Inside React
class Component {}
Component.isReactClass = {};
// We can check it like this
class Greeting extends Component {}
console.log(Greeting.isReactClass); // ✅ 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); // ✅ Yes
複製代碼
React就是這樣解決的。
後面還有幾段,參考文末另外一位大兄弟的譯文吧。
這文章有點長,涉及的知識點也比較多,最後的解決方案,看似挺簡單的,實際上走到這一步並不簡單,但願你們都有所收穫。 翻譯到一半的時候,在React的一個Issues中發現另外一我的這篇文章的譯文,有興趣的童鞋,能夠點擊閱讀。