React怎样从函数中辨别类

栏目: IOS · Android · 发布时间: 7年前

内容简介:考虑用函数定义的组件React也支持使用类定义它:(直到最近,那是唯一的方式使用state特性)

考虑用函数定义的组件 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而不是React。

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

这是一段较长的旅程。这篇博客不会有很多关于React的信息,但是我们会审查某些方面 newthisclassarrow functionsprototype__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仅仅检查某个东西是否是类吗?

不会这么简单的!尽管我们 在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所做的事情。

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

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

我们知道当说 obj.foo,JavaScript 真的在 objobj.__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 早期版本(在 JestGood™ ️之前)中会默认的自动模拟。生成的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


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

通灵芯片

通灵芯片

Daniel Hillis / 崔良沂 / 上海世纪出版集团 / 2009-1 / 19.80元

本书深入浅出地阐述了计算机科学中许多基本而重要的概念,包括布尔逻辑、有限自动机、编程语言、图灵机的普遍性、信息论、算法、并行计算、量子计算、神经网络、机器学习乃至自组织系统。 作者高屋建瓴式的概括,既不失深度,又妙趣横生,相信读者读后会有很多启发。 目录: 序言:石的奇迹 第一章 通用件 第二章 万能积木 第三章 程序设计 第四章 图灵机的普适性 第......一起来看看 《通灵芯片》 这本书的介绍吧!

MD5 加密
MD5 加密

MD5 加密工具

XML 在线格式化
XML 在线格式化

在线 XML 格式化压缩工具

RGB CMYK 转换工具
RGB CMYK 转换工具

RGB CMYK 互转工具