原文 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。
可是若是你很好奇,能帮助你理解它如何工做也很棒。