内容简介:原文链接:本文中通过探讨这个问题,涉及到了JavaScript中大量的重要概念像原型、原型链、this、类、继承等,通过思考这个问题对这些知识进行一个回顾,不失为一个好的学习方法,但如果你只是想知道这个问题的答案,就像作者说的那样,直接滚动到底部吧。限于本人水平有限,翻译不到位的地方,敬请谅解。
原文链接: How Does React Tell a Class from a Function?
本文中通过探讨这个问题,涉及到了JavaScript中大量的重要概念像原型、原型链、this、类、继承等,通过思考这个问题对这些知识进行一个回顾,不失为一个好的学习方法,但如果你只是想知道这个问题的答案,就像作者说的那样,直接滚动到底部吧。
限于本人水平有限,翻译不到位的地方,敬请谅解。
正文
在React中我们可以用Function定义一个组件:
function Greeting() { return <p>Hello</p>; } 复制代码
同样可以使用Class定义一个组件:
class Greeting extends React.Component { render() { return <p>Hello</p>; } } 复制代码
在React推出Hooks之前,Class定义的组件是使用像state这样的功能的唯一方式。
当你想渲染的时候,你不需要关心它是怎样定义的:
// Class or function — whatever. <Greeting /> 复制代码
但是React会关心这些不同。
如果 Greeting
是一个函数,React需要像下面这样调用:
// Your code function Greeting() { return <p>Hello</p>; } // Inside React const result = Greeting(props); // <p>Hello</p> 复制代码
但是如果 Greeting
是一个类,React需要用 new
命令创建一个实例,然后调用创建的实例的 render
方法:
// Your code class Greeting extends React.Component { render() { return <p>Hello</p>; } } // Inside React const instance = new Greeting(props); // Greeting {} const result = instance.render(); // <p>Hello</p> 复制代码
那么React是怎么分辨 class
或者 function
的呢?
这会是一个比较长的探索之旅,这篇文章不会过多的讨论React,我们将探索 new,this,class,箭头函数,prototype,__proto__,instanceof
的某些方面以及它们是怎么在JavaScript中一起工作的。
首先,我们需要理解为什么区分functions和class之间不同是如此重要,注意怎样使用 new
命令去调用一个class:
// 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中究竟是干什么的。
之前JavaScript并没有Class,但是你能用一个正常的函数去模拟Class。 具体地说,你可以使用任何通过new调用的函数去模拟class的构造函数 :
// Just a function function Person(name) { this.name = name; } var fred = new Person('Fred'); // :white_check_mark: Person {name: 'Fred'} var george = Person('George'); // :red_circle: Won’t work 复制代码
现在你仍然可以这样写,麻溜试一下哟。
如果你不用new命令调用 Person('Fred')
,函数中this会指向 window
或者 undefined
,这样我们的代码将会炸掉或者出现怪异的行为像设置了 window.name
。
通过使用new命令调用函数,相当于我们说:“JavaScript,你好,我知道 Person
仅仅只是一个普通函数但是让我们假设它就是类的一个构造函数。创建一个 {}
对象然后传入 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添加Class(类)之前是怎样模拟Class(类)的。
如果你定义了一个函数,JavaScript是不能确定你会像 alert()
一样直接调用或者作为一个构造函数像 new Person()
。忘了使用new命令去调用像 Person
这样的函数将会导致一些令人困惑的行为。
Class(类)的语法相当于告诉我们:“这不仅仅是一个函数,它是一个有构造函数的类”。如果你在调用Class(类)的时候,忘了加new命令,JavaScript将会抛出一个错误:
et fred = new Person('Fred'); // :white_check_mark: If Person is a function: works fine // :white_check_mark: If Person is a class: works fine too let george = Person('George'); // We forgot `new` // :flushed: If Person is a constructor-like function: confusing behavior // :red_circle: If Person is a class: fails immediately 复制代码
这将帮助我们及早的发现错误,而不是等到出现明显的bug的时候才知道,像 this.name
变成了 window.name
而不是 george.name
。
不管怎样,这意味着React需要使用new命令去调用所有的类,它不能像调用正常函数一样去调用类,如果这样做了,JavaScript会报错的!
class Counter extends React.Component { render() { return <p>Hello</p>; } } // :red_circle: React can't just do this: const instance = Counter(props); 复制代码
上面是错误的写法。
在我们讲React是怎么解决的之前,我们要知道大多数人会使用Babel去编译React项目,目的是为了让项目中使用的最新特性像class(类)能够兼容低端的浏览器,这样我们就需要了解的Babel的编译机制。
在Babel早期的版本中,class(类)可以使用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'); // :white_check_mark: Okay Person('George'); // :red_circle: Can’t call class as a function 复制代码
你可能有在打包出来的文件中看到过上面的代码,这就是 _classCallCheck
所做的事情。
到目前为止,你应该已经大概掌握了使用new命令和不使用new命令之间的差别:
这就是为什么React需要正确调用组件是如此重要的原因。如果你使用class(类)定义一个组件,React需要使用new命令去调用。
那么React能判断出一个组件是否是由class(类)定义的呢?
没那么容易,即使我们能分辨出函数和class(类):
function isClass(func) { return typeof func === 'function' && /^class\s/.test(Function.prototype.toString.call(func)); } 复制代码
但如果我们使用了像Babel这样的编译工具,上面的方法是不会起作用的,Babel会将class(类)编译为:
// 类 class Person { } // Babel编译后 "use strict"; function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } var Person = function Person() { _classCallCheck(this, Person); }; 复制代码
对于浏览器来说,它们都是普通的函数。
ok,React里面的函数能不能都使用new命令调用呢?答案是不能。
用new命令调用普通函数的时候,会传入一个对象实例作为 this
,像上面的 Person
那样将函数作为构造函数来使用是可以的,但是对于函数式的组件却会让人懵逼的:
function Greeting() { // We wouldn’t expect `this` to be any kind of instance here return <p>Hello</p>; } 复制代码
即使你能这样写,下面的两个原因会杜绝你的这种想法。
第一个原因:使用new命令调用箭头函数(未经Babel编译过)会报错
const Greeting = () => <p>Hello</p>; new Greeting(); // :red_circle: 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} /> ); } } 复制代码
Tips:
如果不太理解的童鞋,可以参考下面的文章
ok,箭头函数没有自己的 this
,这就意味着它不能作为构造函数:
const Person = (name) => { // :red_circle: This wouldn’t make sense! this.name = name; } 复制代码
因此,JavaScript不能使用new命令调用箭头函数,如果你这样做了,程序就会报错,和你不用new命令去调用class(类)一样。
这是非常好的,但是不利于我们的计划,因为箭头函数的存在,React不能只用new命令去调用,当然我们也能试着去通过箭头函数没有 prototype
去区分它们,然后不用new命令调用:
(() => {}).prototype // undefined (function() {}).prototype // {constructor: f} 复制代码
但是如果你的项目中使用了Babel,这也不是个好主意,还有另一个原因使这条路彻底走不通。
这个原因是使用new命令调用React中的函数式组件,会获取不到这些函数式组件返回的字符串或者其他基本数据类型。
function Greeting() { return 'Hello'; } Greeting(); // :white_check_mark: 'Hello' new Greeting(); // :flushed: Greeting {} 复制代码
关于这点,我们需要知道new命令到底干了什么?
通过new操作符调用构造函数,会经历以下4个阶段
- 创建一个新的对象;
- 将构造函数的this指向这个新对象;
- 指向构造函数的代码,为这个对象添加属性,方法等;
- 返回新对象。
关于这些内容在 全方位解读this-这波能反杀 有更为详细的解释。
如果React只使用new命令调用函数或者类,那么就无法支持返回字符串或者其他原始数据类型的组件,这肯定是不能接受的。
到目前为止,我们知道了,React需要去使用new命令调用class(包括经过Babel编译的),不使用new命令调用正常函数和箭头函数,这仍没有一个可行的方法去区分它们。
当你使用class(类)声明一个组件,你肯定想继承 React.Component
中像 this.setState()
一样的内部方法。与其去费力去分辨一个函数是不是一个类,还不如我们去验证这个类是不是 React.Component
的实例。
剧透:React就是这么做的。
可能我们常用的检测 Greeting
是React组件示例的方法是 Greeting.prototype instanceof React.Component
:
class A {} class B extends A {} console.log(B.prototype instanceof A); // true 复制代码
我猜你估计在想,这中间发生了什么?为了回答这个问题,我们需要理解JavaScript的原型。
你可能已经非常熟悉原型链了,JavaScript中每一个对象都有一个“prototype(原型)”。
下面的示例和图来源于 前端基础进阶(九):详解面向对象、构造函数、原型与原型链 ,个人觉得比原文示例更能说明问题
// 声明构造函数 function Person(name, age) { this.name = name; this.age = age; } // 通过prototye属性,将方法挂载到原型对象上 Person.prototype.getName = function() { return this.name; } var p1 = new Person('tim', 10); var p2 = new Person('jak', 22); console.log(p1.getName === p2.getName); // true 复制代码
当我们想要调用p1上的getName方法时,但是p1自身并没有这个方法,它会在p1的原型上寻找,如果没有找到我们会沿着原型链在上一层的原型上继续找,也就是在p1的原型的原型...,一直找下去,直到原型链的终极 null 。
原型链更像 __proto__.__proto__.__proto__
而不是 prototype.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__
。如果你想往原型上添加一些东西,你应该添加到 Person.prototype
上,那添加到 __proto___
可以吗?当然可以,能生效,但是这样不符合规范的,有性能问题和兼容性问题,详情点击这里。
早期的浏览器是没有暴露 __proto
属性的,因为原型类是一个内部的概念,后来一些浏览器逐渐支持,在ECMAScript2015规范中被标准化了,想要获取某个对象的原型,建议老老实实的使用 Object.getPrototypeOf()
。
我们现在已经知道了,当访问 obj.foo
的时候,JavaScript通常在 obj
中这样寻找 foo
, obj.__proto__,obj.__proto__.__proto__
...
定义一个类组件,你可能看不到原型链这套机制,但是 extends(继承)
只是原型链的语法糖,React的类组件就是这样访问到 React.Component
中像 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) 复制代码
换句话说, 当你使用类的时候,一个实例的原型链映射这个类的层级
// `extends` chain Greeting → React.Component → Object (implicitly) // `__proto__` chain new Greeting() → Greeting.prototype → React.Component.prototype → Object.prototype 复制代码
因为原型链映射类的层级,那我们就能从一个继承自 React.Component
的组件 Greeting
的 Greeting.prototype
开始,顺着原型链往下找:
// `__proto__` chain new Greeting() → Greeting.prototype // ️ We start here → React.Component.prototype // :white_check_mark: Found it! → Object.prototype 复制代码
实际上, x instanceof y
就是做的这种查找,它沿着x的原型链查找y的原型。
通常这用来确定某个实例是否是一个类的实例:
let greeting = new Greeting(); console.log(greeting instanceof Greeting); // true // greeting ( ️ We start here) // .__proto__ → Greeting.prototype (:white_check_mark: 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 (:white_check_mark: 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 (:white_check_mark: Found it!) console.log(greeting instanceof Banana); // false // greeting ( ️ We start here) // .__proto__ → Greeting.prototype // .__proto__ → React.Component.prototype // .__proto__ → Object.prototype (:no_good: Did not find it!) 复制代码
并且它也能用来检测一个类是否继承自另一个类:
console.log(Greeting.prototype instanceof React.Component); // greeting // .__proto__ → Greeting.prototype ( ️ We start here) // .__proto__ → React.Component.prototype (:white_check_mark: Found it!) // .__proto__ → Object.prototype 复制代码
我们就能通过这种方式检测出一个组件是函数组件还是类组件。
然而React并没有这样做。
作者此处还探讨了两种方案,在此略去,有兴趣看原文哟。
实际上React对基础的组件也就是 React.Component
添加了一个标记,并通过这个标记来区分一个组件是否是一个类组件。
// Inside React class Component {} Component.isReactClass = {}; // We can check it like this class Greeting extends Component {} console.log(Greeting.isReactClass); // :white_check_mark: Yes 复制代码
像上面这样把标记直接添加到基础组件自身,有时候会出现 静态属性丢失 的情况,所以我们应该把标记添加到 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); // :white_check_mark: Yes 复制代码
React就是这样解决的。
后面还有几段,参考文末另一位大兄弟的译文吧。
后续
这文章有点长,涉及的知识点也比较多,最后的解决方案,看似挺简单的,实际上走到这一步并不简单,希望大家都有所收获。 翻译到一半的时候,在React的一个Issues中发现另一个人这篇文章的译文,有兴趣的童鞋, 可以点击阅读 。
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- CVPR 2019 | 告别低分辨率网络,微软提出高分辨率深度神经网络HRNet
- Flutter图片分辨率适配
- Kali Linux 自定义分辨率
- 人脸超分辨率,基于迭代合作的方法
- 基于深度学习的图像超分辨率重建
- 浅谈AI视频技术超分辨率
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。