博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
React怎样从函数中辨别类
阅读量:6201 次
发布时间:2019-06-21

本文共 11188 字,大约阅读时间需要 37 分钟。

考虑用函数定义的组件Greeting:

function Greeting() {    return 

Hello

}复制代码

React也支持使用类定义它:

class Greeting extends React.Component {    render() {        return 

Hello

}}复制代码

(直到,那是唯一的方式使用state特性)

当你render一个<Greeting />的时候,你不需要关心它是如何定义的。

// 类 或 函数
复制代码

但是React自己关心它们的不同!

如果Greeting是一个函数,React需要调用它:

// 你的代码function Greeting() {    return 

Hello

}// React 内部const result = Greeting(props); //

Hello

复制代码

但是如果Greeting是一个类,React需要通过new操作符将它实例化,然后调用实例的render方法:

// 你的代码class Greeting extends React.Component {  render() {    return 

Hello

; }}// React 内部const instance = new Greeting(props); // Greeting {}const result = instance.render(); //

Hello

复制代码

在两种情况下React的目标都是获取被渲染的节点(在这个例子中就是,<p>Hello</p>)。但是具体的步骤取决于Greeting是怎样定义的。

因此React是怎样分辨类和函数的呢?

正如我所说的,不知道这些你也能使用React生产。多年来我都不知道这件事。请不要把这个问题变成面试问题。事实上,这篇文章更多的关于是JavaScript而不是React。

这篇文章是为那些好奇于React为何会在一种特定方式下工作的读者写的。你是那样的读者吗?让我们一起深入探讨吧。

这是一段较长的旅程。这篇博客不会有很多关于React的信息,但是我们会审查某些方面newthisclassarrow functionsprototype__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() {        return 

Hello

}}// 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是任何类型的实例    return 

Hello

}复制代码

不过,这是可以容忍的。有两个其他的原因扼杀了这个想法。


总是使用`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所做的事情。


也许,检测GreetingReact 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。如果我们没有在那里找到,我们将继续沿着原型链查找——fredprototypeprototype。等等。

疑惑的是,类或函数的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.prototypeundefined,因为fred不是一个函数)。个人看来,我认为这是即使有经验的开发人员也容易误解JavaScript原型的最大原因。


这是一篇很长的文章,嗯哼?我已经说了80%的东西了。继续。

我们知道当说obj.foo,JavaScript真的在objobj.__proto__, obj.__proto__.__proto__......中查找foo

对于类,你不会直接暴露于这种机制中,但是,extends也可以在良好的旧原型链之上工作。这就是我们的React类实例访问setState等方法的方式:

class Greeting extends React.Component {    render() {        return 

Hello

; }}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早期版本(在JestGood™️之前)中会默认的自动模拟。生成的mocks省略了基本属性,。感谢你,Jest.

近来isReactComponent检查被。

如果你没有继承React.Component,React不会在原型中寻找isReactComponent,也不会将组件视为一个类。现在你知道为什么获得“不能调用类作为一个函数”错误的回答是“extends React.Component”.最后,当prototype.render存在而prototype.isReactComponent不存在时会出现一个。


你也许会说这边文章有点诱导转向法的感觉。真正的解决方案很简单,但是我偏题的解释了大一堆而以这种方法结束,有什么可供选择呢

在我的经验中,库api通常就是这种情况。要使API易于使用,通常需要考虑语言语义(可能是几种语言,包括未来的发展方向),运行时性能,有无编译时间步态的人类工效学,生态系统的状态和打包解决方案,早期警告,以及许多其他东西。最终的结果不一定是最优雅的,但必须是可实践的。

**如果最后API成功了,用户绝不会考虑它的过程。**相反他们会刚专注于创建APPs

但是如果你也好奇,知道它是如何工作是很美妙的一件事情。

原文链接: by

转载于:https://juejin.im/post/5c06565a6fb9a049cd53f795

你可能感兴趣的文章
还在用PS磨皮去皱?看看如何用神经网络高度还原你的年轻容貌!
查看>>
大端模式与小端模式、网络字节顺序与主机字节顺序
查看>>
微信支付申请90%的商户都卡在这儿了,申请微信支付,商户功能设置详细说明...
查看>>
制作一款微信表情
查看>>
高仿Instagram 页面效果android特效
查看>>
我的友情链接
查看>>
Juniper 基于路由的×××
查看>>
HDU - 2018 - 母牛的故事(dp)
查看>>
基于matlab的fft变换中参数的设置
查看>>
如何查找JSP页面中的错误
查看>>
2016 年总结
查看>>
Python学习开始
查看>>
Android应用程序消息处理机制(Looper、Handler)分析(4)
查看>>
C++ 类成员的构造和析构顺序
查看>>
将String转化成Stream,将Stream转换成String
查看>>
java路径Java开发中获得非Web项目的当前项目路径
查看>>
Google API设计指南-资源名称
查看>>
最全React技术栈技术资料汇总(收藏)
查看>>
【工具使用系列】关于 MATLAB 遗传算法与直接搜索工具箱,你需要知道的事
查看>>
flex 学习笔记 stage
查看>>