[译]React如何区别class和function

Posted liuyongjia

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[译]React如何区别class和function相关的知识,希望对你有一定的参考价值。

原文 How Does React Tell a Class from a Function?

译注:

一分钟概览——

React最后采用了在React.Component上加入isReactComponent标识作为区分。

1.在这之前,考虑了ES6的区分方法,但是由于Babel的存在,这个方法不可用。

2.总是调用new,对于一些纯函数组件不适用。而且对箭头函数使用new会出错。

3.把问题约束到React组件下,通过判定原型链来做,但是可能有多个React实例导致判定出错,所以在原型上添加了标识位,标识位是一个对象,因为早期Jest会忽略普通类型如Boolean型。

4.API检测也是可行的,但是API的发展无法预测,每个检测都会带来额外的损耗,所以不是主要做法,但是在现在版本里已经加入了render检测,用来检测prototype.render存在,但是prototype.isReactComponent不存在的场景,这样会抛出一个warning。

以下正文。

思考一下下面这个使用function定义的Greeting组件:

function Greeting() {
  return Hello;
}

React也支持class定义:

class Greeting extends React.Component {
  render() {
    return Hello;
  }
}

(直到最近,这是唯一可以使用类似state这种功能的方法。)
当你在使用<Greeting />组件时,其实并不关心它是怎么定义的。

// Class or function — whatever.

但是React自己是关心这些不同的!
如果Greeting是一个函数,React需要去调用它:

// Your code
function Greeting() {
  return Hello;
}

// Inside React
const result = Greeting(props); // Hello

但是如果Greeting是类,React就需要用new关键字去实例化一个对象,然后立刻调用它的render方法。

// Your code
class Greeting extends React.Component {
  render() {
    return Hello;
  }
}

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

React有一个相同的目的——得到一个渲染完毕的node(在这个例子里,<p>Hello</p>)。但是如果定义Greeting决定了剩下的步骤。
所以React是如何知道一个组件是类还是函数?
就像我之前的博客,你不需要知道这个东西对于React而言的效果。我同样好几点不了解这些。请不要把这个问题变成一个面试题。事实上,比起React,这篇博客更关注于javascript
这篇博客写给那些富有好奇心的读者,他们想知道为什么React能以一种确定的方式工作。你是这样的人吗?一起深入探讨吧!
这是一段漫长的旅程。这篇博客不会写很多关于React的东西,但是会一掠JavaScript本身的风采,诸如:new,this,class,箭头函数,prototype,__proto__,instanceof,以及这些东西如何在JavaScript中合作。幸运的是,在你使用React的时候,你不必想太多这些事。


首先,我们需要明白为什么区分函数和类如此重要。注意我们怎么使用new操作符去调用一个类:

// If Greeting is a function
const result = Greeting(props); // Hello

// If Greeting is a class
const instance = new Greeting(props); // Greeting {}
const result = instance.render(); // Hello

先来对new操作符做了什么给出一个粗浅的定义。


以前,JavaScript没有类的概念。然而,你也可以用纯函数去描述一种近似于类的模式。具体而言,你可以在调用函数之前,添加new,就可以使用任何类似于类构造器的函数了。

// Just a function
function Person(name) {
  this.name = name;
}

var fred = new Person(‘Fred‘); // ? Person {name: ‘Fred‘}
var george = Person(‘George‘); // ?? Won’t work

直到现在,你还是可以这么写,在调试工具里试一下吧。
如果不使用new,直接调用Person(‘Fred‘),函数内部的this就会指向一些全局变量,也没什么用了(例如:window或undefined)。所以我们的代码就会奔溃,或者做些蠢事像是设置了window.name
通过添加一个new操作符,就像告诉编译器:“Hey,JavaScript,我知道Person只是一个函数,但是请假装它是一个类构造器。去创建一个实例对象,然后把this指向这个对象,这样就可以把this.name指向这个对象了。最后把这个对象的引用给我。”
new操作符大概做了这些事。

var fred = new Person(‘Fred‘); // Same object as `this` inside `Person`

new操作符也让所有Person.prototype的东西都可以被fred对象访问。

function Person(name) {
  this.name = name;
}
Person.prototype.sayHi = function() {
  alert(‘Hi, I am ‘ + this.name);
}

var fred = new Person(‘Fred‘);
fred.sayHi();

这是大家在JavaScript直接支持类特性之前模拟的方法。


所以new在JavaScript中存在很久了,而class则是比较新的特性。我们重写这些代码,来更加贴近我们的想法。

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())一样对待它。如果对类似Person 这样的函数忘记指定new操作符,会带来令人费解的表现。

类语法让我们可以告诉编译器:“这不仅是一个函数,它是一个类并且拥有一个构造器。”如果你忘了调用new ,JavaScript就会抛出一个错误。

let fred = new Person(‘Fred‘);
// ?  If Person is a function: works fine
// ?  If Person is a class: works fine too

let george = Person(‘George‘); // We forgot `new`
// ?? If Person is a constructor-like function: confusing behavior
// ?? If Person is a class: fails immediately

这就可以是我们及时发现一些古怪的错误,比如,this 被指向了window而不是我们期望的george

然而,这也意味着React需要在实例任何类对象之前调用new。如同前面而言,如果少了这一步,就会抛出错误。

class Counter extends React.Component {
  render() {
    return Hello;
  }
}

// ?? React can‘t just do this:
const instance = Counter(props);

这是个大麻烦。

在查看React如何解决这个问题之前,应该清楚,大部分人为了让代码可以跑在旧浏览器里,通常使用Babel或者其他编译器去处理类似class这种现代语法。所以在我们的设计里,必须要考虑编译器。

在Babel早前的版本里,类能够不通过new去调用。然而,这个bug最后被修复了——通过生成下面这些代码。

function Person(name) {
  // A bit simplified from Babel output:
  if (!(this instanceof Person)) {
    throw new TypeError("Cannot call a class as a function");
  }
  // Our code:
  this.name = name;
}

new Person(‘Fred‘); // ? Okay
Person(‘George‘);   // ?? Can’t call class as a function

你也许在构建之后的bundle里看到过这样的代码。这是所有_classCallCheck 函数所做的事情。(你可以选择“loose mode(宽松模式)”来使得编译器绕过这些检查,但可能会使得最终生成的class代码很复杂。)


到目前为止,你应该大概了解了有new和无new 的区别。

new Person() Person()
class ?thisPerson实例 ??TypeError
function ?thisPerson实例 ??this指向windowundefined

这就是React正确调用组件的重要之处。如果通过class声明组件,就必须使用new去调用它。

所以这样React就能检查是否是class了吗?

没那么简单!即使我们可以区别ES6 class和function,但是这样并不能判断Babel这样的工具生成的代码。对于浏览器而言,他们都只是函数而言。真是不走运。


好吧,那React只能每次都使用new了吗?然而,这样也不行。

对于一般的函数,如果通过new去调用,就会新建一个对象实例并将this 指向它。对于写成构造器的函数(就像Person),这样做是可行的,但是对于一般的函数而言,就很奇怪了。

function Greeting() {
  // We wouldn’t expect `this` to be any kind of instance here
  return Hello;
}

这样虽然是可以容忍的,但是还有两个问题使得我们不得不抛弃这种做法。


第一个问题是对箭头函数使用new ,它并不会被Babel处理,直接加new会抛出一个错误。

const Greeting = () => Hello;
new Greeting(); // ?? Greeting is not a constructor

这种表现是符合预期的,也是符合箭头函数设计的。箭头函数的特殊点之一就是没有自己的this值,它只能从最近的函数闭包内获取this值。

class Friends extends React.Component {
  render() {
    const friends = this.props.friends;
    return friends.map(friend =>
      
    );
  }
}

OK,即使箭头函数没有自己的this值,但是也不意味着它完全不能用作构造器!

const Person = (name) => {
  // ?? This wouldn’t make sense!
  this.name = name;
}

因此,JavaScript不允许使用new去调用箭头函数。如果这么做,就会尽早的抛出一个错误。这和不能不用new去调用一个类有点类似。

这个设计很好但是却影响了我们的计划。React不能在所有东西上都加上new,因为这样可能会破坏箭头函数。我们可以通过检测prototype去区分箭头函数和普通函数。

(() => {}).prototype // undefined
(function() {}).prototype // {constructor: f}

但是这样对Babel转移后的函数并不好使。这也不算是大问题,但是还有一个问题让我们彻底放弃了这个想法。


另一个原因在于使用new之后,React就无法支持那些返回string这种基本类型的函数了。

function Greeting() {
  return ‘Hello‘;
}

Greeting(); // ? ‘Hello‘
new Greeting(); // ?? Greeting {}

这是new操作符的另一个怪异设计。正如我们之前看到的,new 告诉JavaScript引擎创建一个对象并把this 指向它,之后将它返回给我们。

然而,JavaScript允许被new调用的函数重载,返回其他对象。大概是在重用实例时,这种池模式比较方便。

// Created lazily
var zeroVector = null;

function Vector(x, y) {
  if (x === 0 && y === 0) {
    if (zeroVector !== null) {
      // Reuse the same instance
      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 同样会忽略那些非对象类型的返回值。如果只是return一个string或者nunber,就像没写return一样。

function Answer() {
  return 42;
}

Answer(); // ? 42
new Answer(); // ?? Answer {}

如果用了new 调用函数,就没有什么办法获得一个基本类型的return。所以,如果React一直用new 调用函数,直接返回string的函数将不能正常使用。

这是不可接受的,所以需要妥协一下。

到目前为止,我们学到了什么?React需要使用new 去调用classes(包括Babel转移后的),但是还需要不用new 直接调用一般函数和箭头函数。但是却没有一种可靠的方法区分它们。

如果不能提出通用解法,是不是可以把问题再细分一下?

当你使用class去定义一个组件,你一般会使用继承React.Component ,然后去使用一些内建方法,比如this.setState()。与其检测全部的class,不如只检测React.Component的子类呢?

剧透:这也是React的做法。

一般而言,检查子类通用的做法就是使用instance of。如果检查Greeting 是不是React组件,就需要使用Greeting.prototype instanceof React.Component

class A {}
class B extends A {}

console.log(B.prototype instanceof A); // true

我知道你在想什么。这里发生了什么?为了解答这个问题,我们需要明白JavaScript原型机制。

你可能听说过“原型链”。每个JavaScript对象都可能有一个“prototype”。当调用fred.syaHi()时,如果fred 上没有sayHi(),就会在它的原型上去寻找。如果没找到,则继续向上找,就像链条一样。

令人费解的事,类或者函数的prototype属性并不是指向当前值得prototype。我没开玩笑。

function Person() {}

console.log(Person.prototype); // ?? Not Person‘s prototype
console.log(Person.__proto__); // ?? Person‘s prototype
// 更像是
__proto__.__proto__.__proto__
// 而不是
prototype.prototype.prototype

原型在函数或者类上到底是啥?通过new实例化的对象都有__proto__属性。

function Person(name) {
  this.name = name;
}
Person.prototype.sayHi = function() {
  alert(‘Hi, I am ‘ + this.name);
}

var fred = new Person(‘Fred‘); // Sets `fred.__proto__` to `Person.prototype`

然后__proto__链展示了JavaScript如何寻找链上的属性和方法。

fred.sayHi();
// 1. Does fred have a sayHi property? No.
// 2. Does fred.__proto__ have a sayHi property? Yes. Call it!

fred.toString();
// 1. Does fred have a toString property? No.
// 2. Does fred.__proto__ have a toString property? No.
// 3. Does fred.__proto__.__proto__ have a toString property? Yes. Call it!

实际上,在代码里几乎不需要操作__proto__,除非需要调试原型链相关的东西。如果你想要将一些属性在fred.__proto__,你应该把它放在Person.prototype。至少这是最初的设计。

浏览器曾经不会把__proto__属性暴露出来,因为原型链被认为是一个内部概念。一些浏览器添加了对__proto__的支持,后续艰难地标准化了,但是为了支持Object.getPrototypeOf() 又会被移出标准。

我仍然觉得很困惑,一个属性称为原型但不给你一个有用的原型(例如,fred.prototype是undefined,因为fred不是一个函数)。就我而言,我认为最大的原因是,,哪怕是有经验的开发人员也常常会误解JavaScript原型。


这篇博客太长了,已经讲完80%了。继续。

我们知道对于obj.foo,JavaScript实际上去寻找objfoo,找不到再去obj.__proto__obj.__proto__.__proto__……

通过使用class,没有必要直接去使用这个机制,extends在原型链下也能工作的很好。下面的例子讲述了为什么React类实例能获取像setState这样的方法。

class Greeting extends React.Component {
  render() {
    return Hello;
  }
}

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)

换句话说,当你使用class,一个实例的__proto__链和类层次一一对应。

// `extends` chain
Greeting
  → React.Component
    → Object (implicitly)

// `__proto__` chain
new Greeting()
  → Greeting.prototype
    → React.Component.prototype
      → Object.prototype

通过类层次和__proto__链的一一对应,我们可以循着原型链找到父级。

// `__proto__` chain
new Greeting()
  → Greeting.prototype // ??? We start here
    → React.Component.prototype // ? Found it!
      → Object.prototype

x instanceof Y就是使用__proto__链进行查找。就是在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做的。

需要注意的是,instanceof不能用来识别页面上继承自两个React基类的实例。在同一个页面上,有两个React实例,是一个错误的设计,但是历史包袱毕竟可能存在,所以还是要避免在这种情况下使用instanceof。(通过使用Hooks,我们可能需要强制维持两份环境了。)

另一种方法可以检测render() 的存在,但是有个问题,无法预测日后API的变化。每次检测都要花费时间,不希望以后API发生变化之后,又加一个。而且,如果实例上声明了render(),也会绕过这个检测。

所以,React在基类上增加了一个特殊的标识。React检测这个标识的存在,这样区别是否是React Component。

起初,这个标识依赖于React.Component本身。

// Inside React
class Component {}
Component.isReactClass = {};

// We can check it like this
class Greeting extends Component {}
console.log(Greeting.isReactClass); // ? Yes

然而,有点类实现不会拷贝静态属性,或者实现了一个不标准的__proto__链,所以传着传着,这个标识就丢了。

这也是为什么React把这个标识移到了React.Component.prototype

// Inside React
class Component {}
Component.prototype.isReactComponent = {};

// We can check it like this
class Greeting extends Component {}
console.log(Greeting.prototype.isReactComponent); // ? Yes

你也许会奇怪为什么标识是一个对象而不是Boolean型。实际上没多大区别,但是在早期的Jest版本中,有自动Mock的机制。Mock后的数据会忽略基本类型的属性,会破坏检测。感谢Jest。

isReactComponent至今仍在使用

如果没有继承React.Component,React在原型上没有发现isReactComponent 标识,就会像对待普通类一样对待它。现在就知道为什么Cannot call a class as a function问题下得票最多的回答建议添加extends React.Component。最后,一个警告已经被加入到React中,用来检测prototype.render存在,但是prototype.isReactComponent不存在的场景。


你可能觉得这是一个关于替换的故事。实际的解决办法很简单,但是,我还需要解释为什么要选择这个方案,以及还存在哪些别的选择。

以我的经验,这对于library级别的API来说,是很常见的。为了让API简单易用,你常常需要考虑语义(也许在一些语言里,还包括未来方向),runtime性能,编译与否,时间成本,生态,打包解决方案,及时warning,还有很多事情。最后的方案不一定优雅,但一定经得起考验。

如果API设计得很成功,这些过程对于用户就是透明的。他们可以专注于开发APP。

但是如果你很好奇,能帮助你理解它如何工作也很棒。










以上是关于[译]React如何区别class和function的主要内容,如果未能解决你的问题,请参考以下文章

[译] 如何在React中写出更优秀的代码

React.js 中的 Function 和 Class 组件之间的确切区别是啥? [复制]

译React 优化:虚拟 DOM 详解

如何配置 webpack 以从其他 lerna 包中转译文件(从 create-react-app 中弹出)

react native中'function App()'和'class App extends Component'之间的区别[重复]

进阶 6-5 期[译] Throttle 和 Debounce 在 React 中的应用