阅读 57

React怎样从函数中辨别类

考虑用函数定义的组件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) => {
  // 🔴 这样是没有意义的
  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操作符设计的怪癖有关。正如我们前面看到的,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(); // ✅ 42
new 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 prototype
console.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 <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 // ✅ 找到了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.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组件类还是一个常规函数。


但这不是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中

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


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

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

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

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

原文链接:overreacted.io/how-does-re… by Dan Abreamov

关注下面的标签,发现更多相似文章
评论