(译)React是如何区分Class和Function?

原文地址: how-does-react-tell-a-class-from-a-functionhtml

本文地址: React是如何区分Class和Function?react

边看边翻译 花了2h+... 若是你以为读起来还算通顺不费事 那也算我为你们作了一点小贡献吧git

React调用二者的不一样之处

一块儿来看下这个 function 类型的 Greeting组件:es6

function Greeting() {
  return <p>Hello</p>;
}
复制代码

React 一样支持将它定义为 class 类型:github

class Greeting extends React.Component {
  render() {
    return <p>Hello</p>;
  }
}
复制代码

(直到最近 hooks-intro,这是使用state等特性的惟一方法。)数组

当你想渲染<Greeting />组件时,你没必要关心它是如何定义的:浏览器

//类或者函数,均可以
<Greeting />
复制代码

可是,做为 React自己 是会认为这两个是有不一样之处的。babel

若是Greeting是一个函数,React 只须要直接调用它:ecmascript

// 你的代码
function Greeting() {
  return <p>Hello</p>;
}

// React内
const result = Greeting(props); // <p>Hello</p>
复制代码

可是若是Greeting是一个类,那么 React 就须要使用new来实例化它,而后在实例上调用render方法:ide

// 你的代码
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如何知道某个东西是class类型仍是function类型呢?

事实上,这篇文章更多的是关于JavaScript而不是关于React。 如何你好奇React为什么以某种方式运做,让咱们一块儿挖掘其中的原理。

这是一段漫长的探求之旅。这篇文章没有太多关于React自己的信息,咱们将讨论newthisclassarrow functionprototype__ proto__instanceof这些概念,以及这些东西如何在JavaScript中运做的机制。幸运的是,当你仅仅是使用React时,你不须要考虑这么多。但你若是要深究React……

(若是你真的只想知道答案,请拉动到文章最后。)

为何要用不一样的调用方式?

首先,咱们须要理解以不一样方式处理class和function的重要性。注意咱们在调用类时如何使用new运算符:

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

// If Greeting is a class
const instance = new Greeting(props); // Greeting {}
const result = instance.render(); // <p>Hello</p>
复制代码

让咱们大体的了解下new在Javascript中作了什么:


在ES6以前,Javascript没有class这个概念。可是,可使用纯函数表现出和class类似的模式。 具体来讲,你可使用new来调用相似类构造方法的函数,来表现出和class类似的模式

// 只是一个function
function Person(name) {
  this.name = name;
}

var fred = new Person('Fred'); // ✅ Person {name: 'Fred'}
var george = Person('George'); // 🔴 不会如期工做
//你今天仍然能够写这样的代码!在 `DevTools` 中尝试一下。
复制代码

若是不用new修饰Person('Fred'),Person内部的this在里面会指向 window 或者 undefined 。结果就是代码会崩溃或者像给window.name赋值同样愚蠢。

在调用以前添加new,等于说:“嘿 JavaScript,我知道Person只是一个函数,但让咱们伪装它是个类构造函数。 建立一个{}对象 并在Person函数内将this指向该对象, 这样我就能够赋值像this.name这样的东西。而后把那个对象返回给我。”

上面这些就是new操做符作的事情。

var fred = new Person('Fred'); // `Person`内,相同的对象做为`this`
复制代码

同时new操做符使上面的fred对象可使用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。可是,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()那样充当构造函数。忘记添加new会致使使人困惑的执行结果。

class语法让咱们明确的告诉Javascript:“这不只仅是一个函数 - 它是一个类,它有一个构造函数”。 若是在调用class时忘记使用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.name被视为window.name而不是george.name

可是,这意味着React须要在调用任何class以前使用new。它不能只是将其做为普通函数直接调用,由于JavaScript会将其视为错误!

class Counter extends React.Component {
  render() {
    return <p>Hello</p>;
  }
}

// 🔴 React can't just do this:
const instance = Counter(props);
复制代码

这意味着麻烦(麻烦就是在于React须要区分Class和Function……)。

探究React式如何解决的

babel之类编译工具给解决问题带来的麻烦

在咱们探究React式如何解决这个问题时,须要考虑到大多数人都使用Babel之类的编译器来兼容浏览器(例如转义class等),因此咱们须要在设计中考虑编译器这种状况。

在Babel的早期版本中,能够在没有new的状况下调用类。可是经过生成一些额外的代码,这个状况已经被修复了:

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
复制代码

你或许在打包文件中看到相似的代码,这就是_classCallCheck函数所作的功能。 (您能够经过设置“loose mode”不进行检查来减少捆绑包大小,但这可能会使代码最终转换为真正的原生类变得复杂。)

到如今为止,你应该大体了解使用new与不使用new来调用某些内容之间的区别:

new Person() Person()
class this is a Person instance 🔴 TypeError
function this is a Person instance 😳 this is window or undefined

这之中的区别就是React为何须要正确调用组件的重要缘由。 若是您的组件被定义为类,React在调用它时须要使用new

那么问题来了 React是否能够判断某个东西是否是一个class?

没有那么容易!即便咱们能够在JavaScript es6 中区别class 和 function,这仍然不适用于像Babel这样的工具处理以后的class。由于对于浏览器来讲,它们只是单纯的function而已(class被babel处理后)。


Okay,也许React能够在每次调用时使用new?不幸的是,这也并不老是奏效。

异常状况一:

做为通常function,使用new调用它们会为它们提供一个对象实例做为this。对于做为构造函数编写的函数(如上面的Person),它是理想的,但它会给函数组件带来混乱:

function Greeting() {
  // 咱们不但愿“this”在这里成为任何一种状况下的实例
  return <p>Hello</p>;
}
复制代码

虽然这种状况也是能够容忍的,但还有另外两个缘由能够扼杀一直使用new的想法。

异常状况二:

第一个是箭头函数(未被babel编译时)会使new调用失效,使用new调用箭头函数会抛出一个异常

const Greeting = () => <p>Hello</p>;
new Greeting(); // 🔴 Greeting is not a constructor
复制代码

这种状况时是有意的,而且遵循箭头函数的设计。箭头函数的主要优势之一是它们没有本身的this绑定 - 取而代之的是 this被绑定到最近的函数体中。

class Friends extends React.Component {
  render() {
    const friends = this.props.friends;
    return friends.map(friend =>
      <Friend
        // `this` is resolved from the `render` method
        size={this.props.size}
        name={friend.name}
        key={friend.id}
      />
    );
  }
}
复制代码

Okay,因此箭头功能没有本身的this 这意味着箭头函数没法成为构造者!

const Person = (name) => {
  // 🔴 This wouldn’t make sense!
  this.name = name;
}
复制代码

所以,JavaScript不容许使用new调用箭头函数。若是你这样作,只会产生错误。这相似于JavaScript不容许在没有new的状况下调用类的方式。

这很不错,但它也使咱们在所有函数调用前添加new的计划失败。 React不能够在全部状况下调用new,由于它会破坏箭头函数!咱们能够尝试经过缺乏prototype来判断出箭头函数:

(() => {}).prototype // undefined
(function() {}).prototype // {constructor: f}
复制代码

可是这个不适用于使用babel编译的函数。这可能不是什么大问题,但还有另外一个缘由让这种方法失败。

异常状况三:

咱们不能老是使用new的另外一个缘由是,这样作不支持返回字符串或其余原始类型。

function Greeting() {
  return 'Hello';
}

Greeting(); // ✅ 'Hello'
new Greeting(); // 😳 Greeting {}
复制代码

这再次与new的设计奇怪表现有关。正如咱们以前看到的那样,new告诉JavaScript引擎建立一个对象,在函数内部建立该对象,而后将该对象做为new的结果。 可是,JavaScript还容许使用new调用的函数经过返回一些其余对象来覆盖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会彻底忽略函数的返回值。若是你返回一个字符串或一个数字,就好像没有返回同样。

function Answer() {
  return 42;
}

Answer(); // ✅ 42
new Answer(); // 😳 Answer {}
复制代码

当使用new调用函数时,没法从函数中读取原始返回值(如数字或字符串)。所以,若是React老是使用new,它将没法支持返回字符串类型的函数(组件)

这是不可接受的,因此咱们得另寻他法。

解决方式

到目前为止咱们了解到了什么? React须要用new调用类(兼容Babel状况),但它须要调用常规函数或箭头函数(兼容Babel)时不能使用new。同时并无可靠的方法来区分它们。 若是咱们没法解决一个广泛问题,那么咱们能解决一个更具体的问题吗?

将Component定义为class时,你可能但愿继承React.Component使用其内置方法(好比this.setState())。那么咱们能够只检测React.Component子类,而不是尝试检测全部class吗?

剧透:这正是React所作的。

prototype__proto__

也许,判断Greeting是不是React component class的通常方法是测试Greeting.prototype instanceof React.Component

class A {}
class B extends A {}

console.log(B.prototype instanceof A); // true
复制代码

我知道你在想什么。刚刚发生了什么?!要回答这个问题,咱们须要了解JavaScript原型。

你可能熟悉原型链。JavaScript中的每一个对象均可能有一个“原型”。当咱们编写fred.sayHi()fred对象没有sayHi属性时,咱们在fred的原型上查找sayHi属性。若是咱们在那里找不到它,咱们会看看链中的下一个原型--fred的原型的原型。并以此类推。

使人困惑的是,类或函数的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__ 都指向其构造器的prototype 函数或类的prototype属性就是这样一个东西

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__,最终它被勉强标准化。

至今我仍然以为“prototype的属性没有给你一个值的原型“很是使人困惑(例如,fred.prototype未定义,由于fred不是一个函数)。就我的而言,我认为这是致使经验丰富的开发人员也会误解JavaScript原型的最大缘由。

extends 与 原型链

这帖子有点长 不是吗?别放弃!如今已经讲了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)
复制代码

换句话说,类实例的__protp__链会镜像拷贝类的继承关系:

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

// `__proto__` chain
new Greeting()
  → Greeting.prototype
    → React.Component.prototype
      → Object.prototype
复制代码

如此两个链(继承链 原型链)

instanceof 判断方式

因为__proto__链镜像拷贝类的继承关系,所以咱们能够经过Greeting的原型链来判断Greeting是否继承了React.Component

// `__proto__` chain
new Greeting()
  → Greeting.prototype // 🕵️ We start here
    → React.Component.prototype // ✅ Found it!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副本的React.Component,这种instanceof方式就会失效。 在一个项目中混合使用React的多个副本是很差的方式,但咱们应该尽量避免出现因为历史遗留所产生的这种问题。 (使用Hooks,咱们可能须要强制删除重复数据。)

另外一种可能的骚操做是检查原型上是否存在render方法。可是,当时还不清楚组件API将如何变换。每一个判断操做都有成本咱们不想添加多于一次的操做。若是将render定义为实例方法(例如使用类属性语法),这也不起做用。

所以,React为基本组件添加了一个特殊标志。React经过检查是该标志来判断一个东西是不是React组件类。

最初该标志在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
复制代码

这就是React如何判断class的所有内容。


现在在React中使用就是isReactComponent标志检查。

若是不扩展React.Component,React将不会在原型上找到isReactComponent,也不会将组件视为类。如今你知道为何Cannot call a class as a function问题最受欢迎的回答是添加extends React.Component。最后,添加了一个prototype.render存在时,但prototype.isReactComponent不存在的警告

实际的解决方案很是简单,但我用大量的时间解释了为何React最终采起这个解决方案,以及替代方案是什么。 你可能以为博文这个解释过程有点啰嗦,

根据个人经验,开发库API常常会遇到这种状况。为了使API易于使用,开发者须要考虑语言语义(可能,对于多种语言,包括将来的方向)、运行时性能、是否编译状况的兼容、完总体系和打包解决方案的状态、 早期预警和许多其余事情。 最终结果可能并不优雅,但必须实用。

若是最终API是成功的,则用户永远没必要考虑此过程。 取而代之的是他们只须要专一于建立应用程序。

但若是你也好奇......去探究其中的缘由仍是十分有趣的。

相关文章
相关标签/搜索