考虑用函数定义的组件Greeting
:
function Greeting() { returnHello
}复制代码
React也支持使用类定义它:
class Greeting extends React.Component { render() { returnHello
}}复制代码
(直到,那是唯一的方式使用state特性)
当你render一个<Greeting />
的时候,你不需要关心它是如何定义的。
// 类 或 函数复制代码
但是React自己关心它们的不同!
如果Greeting
是一个函数,React需要调用它:
// 你的代码function Greeting() { returnHello
}// React 内部const result = Greeting(props); //Hello
复制代码
但是如果Greeting
是一个类,React需要通过new
操作符将它实例化,然后调用实例的render
方法:
// 你的代码class Greeting extends React.Component { render() { returnHello
; }}// React 内部const instance = new Greeting(props); // Greeting {}const result = instance.render(); //Hello
复制代码
在两种情况下React的目标都是获取被渲染的节点(在这个例子中就是,<p>Hello</p>
)。但是具体的步骤取决于Greeting
是怎样定义的。
因此React是怎样分辨类和函数的呢?
正如我所说的,不知道这些你也能使用React生产。多年来我都不知道这件事。请不要把这个问题变成面试问题。事实上,这篇文章更多的关于是JavaScript而不是React。
这篇文章是为那些好奇于React为何会在一种特定方式下工作的读者写的。你是那样的读者吗?让我们一起深入探讨吧。
这是一段较长的旅程。这篇博客不会有很多关于React的信息,但是我们会审查某些方面new
,this
,class
,arrow functions
,prototype
,__proto__
,instanceof
,和这些东西在JavaScript
中是如何一起工作的。幸运的是,当你使用React时你不需要考虑太多这方面的问题。如果你正在实现那么...
(如果你只是想要知道结果,请导航到最底部。)
首先,我们需要了解为什么将函数和类视为不同的是重要的。注意我们怎样使用new操作符当调用一个类时:
// 如果Greeting是一个函数const result = Greeting(props); //Hello
// 如果Greeting是一个类const instance = new Greeting(props); // Greeting {}const result = instance.render(); //Hello
复制代码
让我们粗略了解一下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() { returnHello
}}// 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'); // OkayPerson('George'); // 不能调用类像函数一样复制代码
你可能有一些代码像这样在你的包中。那些都是_classCallCheck
函数所做的事情。(你可以通过选择“松散模式”来减少包的大小,而无需进行检查,但是这可能会使你最终转换到真正的本地类的过程变得复杂。)
到目前为止,你应该大致了解了使用new或不使用new调用某些东西的区别:
New Person() Person() class this是一个Person实例 TypeError function this是一个Person实例 this是window或者undefined这就是为什么正确调用组件的React非常重要。如果你的组件被定义为一个类,则React在调用它时需要使用new
。
那么React仅仅检查某个东西是否是类吗?
不会这么简单的!尽管我们,这仍然不适用于Babel等工具处理的类。对于浏览器来说,它们只是普通函数。对React是不好的。
好吧,也许React在每次调用时都要使用new
?不幸的是,这也不总是有效的。
对于常规函数,用new
调用它们会给它们一个this
的对象实例。对于作为构造函数编写的函数(如我们上面提到的Person)来说,它是可取的,但是对于函数组件来说,它可能会令人混淆:
function Greeting() { // 我们不期望this是任何类型的实例 returnHello
}复制代码
不过,这是可以容忍的。有两个其他的原因扼杀了这个想法。
总是使用`new`不起作用的第一个原因是本地箭头函数(不是`Babel`编译的那些函数)),调用使用`new`会抛出一个错误:
const Greeting = () =>Hello
;new Greeting(); // Greeting不是一个构造器复制代码
这种行为是有意的,并遵循箭头函数的设计。箭头函数的一个主要好处是它们没有自己的this
值——相反,this
的值指向最近的常规函数:
class Friends extends React.Component { render() { const friends = this.props.friends; return friends.map(friend =>); }}复制代码
箭头函数没有自己的this
。这意味着它们作为构造函数将完全无用!
const Person = (name) => { // ? 这样是没有意义的 this.name = name;}复制代码
因此,JavaScript不允许使用new
调用箭头函数。如果你这样做了,无论如何你都可能犯了一个错误,最好早点告诉你。这类似于JavaScript在没有new
的情况下不允许调用类。
这很好,但也破坏了我们的计划。React不能对所有东西都调用new
,因为它会破坏箭头函数!我们可以通过箭头函数缺少原型来检测它们,而不仅仅new
一个:
(() => {}).prototype // undefined(function() {}).prototype // {constructor: f}复制代码
但这于Babel编译的函数。这可能不是什么大问题,但还有一个原因使这种方法成为死胡同。
我们不能总是使用new的另一个原因是,它将阻止对返回字符串或其他基本类型的组件的支持。
function Greeting() { return 'Hello';}Greeting(); // ✅ 'Hello'new Greeting(); // ? Greeting {}复制代码
这又一次与设计的怪癖有关。正如我们前面看到的,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); // ? b === c复制代码
然而,new
也会完全忽视函数返回非对象的值。如果你返回一个字符串或者数字,就像没有返回一样。
function Answer() { return 42;}Answer(); // ✅ 42new Answer(); // ? 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 prototypeconsole.log(Person.__proto__); // ? 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() { returnHello
; }}let c = new Greeting();console.log(c.__proto__); // Greeting.prototypeconsole.log(c.__proto__.__proto__); // React.Component.prototypeconsole.log(c.__proto__.__proto__.__proto__); // Object.prototypec.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` chainGreeting → React.Component → Object (implicitly)// `__proto__` chainnew Greeting() → Greeting.prototype → React.Component.prototype → Object.prototype复制代码
2种链式
因为__proto__
链反映了类的层次结构,我们可以根据Greeting.prototype
检查Greeting
是否继承自React.Component
,然后跟着__proto__
链找下去:
// `__proto__` chainnew Greeting() → Greeting.prototype // ?从这里开始 → React.Component.prototype // ✅ 找到了 → 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 (✅ Found it!)// .__proto__ → React.Component.prototype // .__proto__ → Object.prototypeconsole.log(greeting instanceof React.Component); // true// greeting (?️ We start here)// .__proto__ → Greeting.prototype// .__proto__ → React.Component.prototype (✅ Found it!)// .__proto__ → Object.prototypeconsole.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的副本时,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.Component
,React不会在原型中寻找isReactComponent
,也不会将组件视为一个类。现在你知道为什么获得“不能调用类作为一个函数”错误的回答是“extends React.Component
”.最后,当prototype.render
存在而prototype.isReactComponent
不存在时会出现一个。
你也许会说这边文章有点诱导转向法的感觉。真正的解决方案很简单,但是我偏题的解释了大一堆而以这种方法结束,有什么可供选择呢
在我的经验中,库api
通常就是这种情况。要使API
易于使用,通常需要考虑语言语义(可能是几种语言,包括未来的发展方向),运行时性能,有无编译时间步态的人类工效学,生态系统的状态和打包解决方案,早期警告,以及许多其他东西。最终的结果不一定是最优雅的,但必须是可实践的。
**如果最后API
成功了,用户绝不会考虑它的过程。**相反他们会刚专注于创建APPs
。
但是如果你也好奇,知道它是如何工作是很美妙的一件事情。
原文链接: by