[译]React如何区别class和function
Posted liuyongjia
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[译]React如何区别class和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 |
?this 是Person 实例 |
??TypeError |
function |
?this 是Person 实例 |
??this 指向window 或undefined |
这就是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实际上去寻找obj
的foo
,找不到再去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.js 中的 Function 和 Class 组件之间的确切区别是啥? [复制]
如何配置 webpack 以从其他 lerna 包中转译文件(从 create-react-app 中弹出)
react native中'function App()'和'class App extends Component'之间的区别[重复]