[译]React如何区别class和function

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

译注:html

一分钟概览——java

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

1.在这以前,考虑了ES6的区分方法,可是因为Babel的存在,这个方法不可用。react

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

3.把问题约束到React组件下,经过断定原型链来作,可是可能有多个React实例致使断定出错,因此在原型上添加了标识位,标识位是一个对象,由于早期Jest会忽略普通类型如Boolean型。github

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。

可是若是你很好奇,能帮助你理解它如何工做也很棒。

相关文章
相关标签/搜索