React怎样从函数中辨别类

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

内容简介:考虑用函数定义的组件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


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

查看所有标签

猜你喜欢:

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

C语言深度解剖

C语言深度解剖

陈正冲 / 北京航空航天大学出版社 / 2010-7 / 29.00元

《C语言深度解剖:解开程序员面试笔试的秘密》由作者结合自身多年嵌入式c语言开发经验和平时讲解C语言的心得体会整理而成,其中有很多作者独特的见解或看法。由于并不是从头到尾讲解C语言的基础知识,所以《C语言深度解剖:解开程序员面试笔试的秘密》并不适用于C语言零基础的读者,其内容要比一般的C语言图书深得多、细致得多,其中有很多问题是各大公司的面试或笔试题。 《C语言深度解剖:解开程序员面试笔试的秘......一起来看看 《C语言深度解剖》 这本书的介绍吧!

Base64 编码/解码
Base64 编码/解码

Base64 编码/解码

XML、JSON 在线转换
XML、JSON 在线转换

在线XML、JSON转换工具

HEX HSV 转换工具
HEX HSV 转换工具

HEX HSV 互换工具