// Just a function
function Person(name) {
this.name = name;
}
var fred = new Person('Fred'); // ✅ Person {name: 'Fred'}
var george = Person('George'); // 🔴 Won’t work
function getName(){
this.name = 1;
return {}; // 返回对象
}
var a = new getName;
console.log(a.name); //undefined
function getName(){
this.name = 1;
return 2; // 返回非对象
}
var d = new getName;
console.log(d.name); //1
------
这就是当你使用`new`来调用函数后会执行的一些操作:
```js
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();
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
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
// 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
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`
JavaScript就是依靠__proto__链来查找属性的:
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!
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)
instanceof有个问题是在某个场景下会导致得不到正确结果,即当一个页面存在多个不同的React副本。在一个项目中混合使用不同版本的React是不好的,原因有很多,但从长远地看,我们应该尽可能避免出现这种问题。 (With Hooks, we might need to force deduplication though.)PS: 这段不知道该如何翻译比较好...
// Inside React
class Component {}
Component.isReactClass = {};
// We can check it like this
class Greeting extends Component {}
console.log(Greeting.isReactClass); // ✅ Yes
但是一些对class的编译会忽略掉对class静态属性的复制,所以你有可能获取不到这个特殊标志。
这也是为什么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就不会去查找isReactComponent这个属性即我们也不会把它当做一个类组件来处理。现在你可能知道关于“Cannot call a class as a function”这个问题在stackoverflow中得票最高的答案为什么是add extends React.Component了吧。当存在render方法却又找不到isReactComponent属性时会触发警告!
现在我们有一个通过函数定义的
Greeting
组件:同样我们也可以通过类来声明这样一个组件:
一直到最近react-hooks的出现,否则在此之前,如果你的组件需要
state
,你就必须使用类来创建组件。当然当你渲染一个
<Greeting /
组件时,你不需要关心它到底是一个函数式组件还是一个类组件:但是
React
需要关心这两者之间的区别!如果
Greeting
是一个函数式组件,React
将这样调用它:但如果
Greeting
是一个类组件,React
就需要先通过new
操作符生成一个实例,并调用实例的render
方法:这两种方法最后都是为了获得
<p Hello</p
这个节点,但是获取的过程就要看Greeting
是如何被定义的了。所以,React是如何知道类和函数的区别的?
就像上一篇文章介绍的那样,你不需要知道这一点就可以使用
React
。多年来我也一直都不知道这一点。请不要把这些当做面试题,事实上,这篇文章更多的是在讨论Javascript
而不是React
。这个博客是写给那些好奇
React
究竟是如何运作的人们。你是这其中的一员吗?让我们一起探索吧。我们准备发车了(这不是开往幼儿园的车),路途遥远,请系好安全带。这篇文章不会过多的谈论
React
本身,但是我们会涉及到很多的概念,像new
、this
、class
、箭头函数
、prototype
、__proto__
、instance
,并且我们会去探究这些东西是如何在JavaScript
中一起工作的。幸运的是,如果你只是为了使用React
。你并不需要知道这些细节,是的,不需要掌握这些你也可以使用React
...(如果你已经知道答案,出门右拐吧...不,请
滚
到最底下)在早些时候,
JavaScript
中并没有class(类)
这个玩意,但是你却可以使用函数
来模拟类
的行为。准确地说,你可以像调用类的构造函数一样通过new
来调用函数:在今天,你仍然可以这样书写代码,你可以自己动手试试!
如果你没有通过
new
操作符来调用Person('Fred')
,这将导致this
的指向不明确(例如,this
可能代表着window
或者是undefined
),这可能会导致你的代码不能正确运行。通过
new
来调用函数,就像我们对JavaScript
说:“我知道Person
只是一个函数而已,但是请你假装把它当做一个类的构造函数吧!然后请你创建一个空对象
,并且将this
指向Person
吧,这样我就可以为Person
添加属性了,像this.name
这样的,最后,请你把这个对象返回给我吧”function getName(){
this.name = 1;
return 2; // 返回非对象 } var d = new getName;
console.log(d.name); //1
new
操作符还可以使我们在Person.prototype
上添加的属性在fred
也可以访问的到:这就是在
es6的class
未出来之前我们是如何模拟类(class)的
。new
已经在JavaScript存在了一段时间了,但是class
却出现没多长时间。让我们使用class
重写上面的代码,你会发现它能更好的表达我们的意图:良好的代码语义和清晰的API的设计能帮助我们更好的了解开发者的意图。
如果你声明了一个函数,JavaScript 并不能知道你将怎么调用它,是像
alert()
一样调用还是像new Person()
一样调用。如果你忘记在Person()
前添加new
操作符,这可能导致你的代码不能正确运行。类(class)的语法语义更加清晰,“这不仅仅是一个函数—它是一个class并且它拥有一个构造器”。如果你忘记使用
new
调用class
,JavaScript将抛出一个错误:这能够帮助你尽早的捕获错误,比如
this.name
引用的是window.name
而不是george.name
。但是,这意味着React在调用class的时候必须加上
new
而不能直接调用它,因为JavaScript会将其视为一个错误。似乎我们碰到了些麻烦。
在我们明白React是如何解决这个问题之前,我们要清楚的的知道一点,我们在在使用React的时候,大都会借助Babel这类的编译工具将class这类的新的语言特性编译成能在旧浏览器中使用的代码,这就意味着我们在进行设计的时候要一同考虑到编译器的实现。
在较早版本的Babel,class可以被直接调用而不必通过
new
操作符(这和刚才提到calss的特性相驳)。但是后面Babel修改了对class的编译实现,参考下面这段代码:你可能在Babel编译过后的代码包里看到过一个名为
_classCallCheck
的函数,它的作用就是用来检测class有没有被new
调用而不是直接调用。(你可以通过babel中的一个配置项:loose mode
来减少包体积的大小,而减少的代码中就包括移除了_classCallCheck
,但是这可能在class被原生支持前带来更多的麻烦。)现在,你可以大致地了解了通过和不通过
new
来调用函数的区别了:这对React能否正确调用类组件来说非常重要。如果你声明了一个类组件则React在调用它的时候必须|使用
new
来调用。那么React是如何判断一个函数它到底是不是类(函数模拟出来的)呢?
这似乎没那么容易!尽管我们可以在ES6中分辨出一个function是不是类,但是当我们借助Babel来编译class的时候,得到的是像上面示例代码那样的函数,对于浏览器来说,它们和普通的函数没有任何区别。所以React就不能借此来作判断了~
那如果React全部通过
new
来调用函数呢?不幸的是,这也行不通...当我们通过
new
来调用函数的时候,它会返回一个新对象并将this指向这个对象。如果你是声明一个类这正好满足我们的需求,但是对于函数式来说这就有点问题了:不过这个看起来还可以接受,因为我们不会在函数式组件里面使用this。还有另外两个原因让我们放弃了这个想法。
第一个原因就是当你使用箭头函数来声明一个函数的时候(如果你让Babel不去编译箭头函数的话),通过
new
来调用箭头函数将抛出一个错误:其实之所以会报错是因为箭头函数的设计就是如此。箭头函数没有自己的
this
而是会就近解析this
:所以箭头函数没有自己的this。这就意味着箭头函数没法作为一个构造函数!
因此JavaScript不允许通过
new
来调用箭头函数。如果你通过new
来调用箭头函数,那么你会得到一个错误,JavaScript希望尽早告诉你这个错误从而避免掉上面提到的class没有通过new来调用有可能导致的错误的错误。箭头函数这个特性很好,但是打乱了我们的计划 —“全部通过new来调用函数”。我们又尝试去判断一个函数是否有
prototype
这个属性来判断一个函数是不是箭头函数,而不是通过能否new
调用来判断:悲剧,这同样行不通,因为Babel还是会将箭头函数编译成普通的函数。但是乐观点看这好像没什么大不了(反正我们也不会在函数式组件中使用this),但是还有最后一个原因彻底kill掉了我们这个想法。
另外一个重要的原因是:如果你在函数中返回的并非是一个对象,并且你通过
new
来调用这个函数,神奇的是你拿不到你返回的值如一个字符串或者一个数值:这与new操作符的设计有关,就像我们上面说到的,
new
会执行以下的操作:如果没有显式的返回其他对象,则默认返回这个对象
但是,JavaScript允许你返回一个对象来覆盖掉new创建的那个对象。据我推测,这对一些设计模式像
XX池(比如对象池)
,比如当你想复用一个实例的时候是非常有用的:但是就像刚才说的,如果你返回的不是一个对象,那么抱歉,你拿不到你想要的值,就好像你没有返回任何东西一样...
所以显而易见的,如果React总是通过
new
来调用函数就会出现问题,因为这意味着React必须放弃支持只返回字符串的组件。这是不可接受的,我们必须妥协。
好了,咱们先缕缕刚才都扯了哪些东西。React想实现:
调用普通函数和箭头函数(包括babel编译过后的)的时候又不需要通过
new
调用目前为止,我们没有可靠的方法来区分他们。
如果我们不能解决一般性问题,那么我们可以解决更具体的问题吗?
当你使用类组件的时候你肯定是会需要用到
React.Component
内建的一些方法,比如this.setState
。So,与其判断一个函数究竟是类还是普通函数或者箭头函数,为什么我们不直接判断这个类是不是React.Component
的派生类呢?剧透:React就是这么干滴。
也许,检测
Greeting
组件是不是一个React类组件可以通过Greeting.prototype instanceof React.Component
:我知道你想问为什么代码输出了
true
!为了回答这个问题,我们必须先搞懂JavaScript原型这个东西。你可能对原型链已经很熟悉了。在JavaScript中,每个对象可能都有一个
prototype
属性。当我们希望调用fred.sayHi()
的时候但fred
这个对象又没有sayHi
这个属性的时候,JavaScript会在fred的prototype
上寻找sayHi
这个属性。如果没有找到,那么JavaScript会沿着原型链查找下一个原型即fred
原型的原型。但是令人困惑的是,类或者函数的
prototype
属性并不是指向它真正的原型。我一脸认真脸:所以原型链更准确的说应该是
__proto__.__proto__.__proto__
而不是prototype.prototype.prototype
。这困扰了我好多年。那函数或者类的原型属性到底是什么呢?实际上当我们用
new
来调用函数或者类的时候,返回的那个对象上面有个__proto__
的属性,它指向的就是真正的原型对象。JavaScript就是依靠
__proto__链
来查找属性的:实际上,除非你在调试跟原型或者原型链相关的东西,否则你几乎不需要去访问到
__proto__
属性。请不要直接在fred.__proto__
上添加属性,而是应该在它的构造函数原型Person.prototype
上添加属性,设计就是如此,请不要乱来哦。最初,浏览器甚至连
__proto__
属性都没有暴露,因为原型链被认为是一个内部概念。但是随着一些浏览器添加了__proto__
属性,原型链最终被标准化了。(但是不赞成使用Object.getProtoTypeof()
)。但是我仍然对访问一个对象的
prototype
属性得不到其真正的原型感到困惑(例如:如果fred
不是一个函数的话,访问fred.prototype
得到的是undefined
)。 就我个人而言的话,我认为这是即使是经营丰富的开发人员也容易对JavaScript原型产生误解的重要原因。这文章也太长了吧...还剩20%...Orz
现在我们知道,当我们访问
obj.foo
的时候,JavaScript实际会先在obj
上寻找foo
属性,接着是obj.__proto__, obj.__proto__.__proto__
...如果你直接使用ES6中的类,你可能都不需要理解
__proto__链(原型链)
这个东西,但是extends
就是__proto__链(原型链)
的语法糖。这也解释了为什么类组件可以访问到React.Component
上的属性比如setState
:换句话说,当我们使用class才创建类时,其实例的
__proto__链(原型链)
正一一对应着类的层级结构:2 Chainz. (不知如何翻译)
既然
__proto__链(原型链)
映射出类的层级结构,那我们如果想判断Greeting
是否extends
自React.Component
,则我们就从Greeting.prototype
顺着__proto__链(原型链)
来做一一判断:实际上,
X instanceof Y
就是做的这种查询。它会顺着X.__proto__.__proto__...
查找Y.prototype
。通常,它用于确定某样东西是否是类的实例:
并且,这同样适用于一个类是否
extends
另外一个类:所以我们可以借助这个来检测一个组件究竟是函数式组件或者类组件。
不过,这并不是React所做的。😳
instanceof
有个问题是在某个场景下会导致得不到正确结果,即当一个页面存在多个不同的React副本。在一个项目中混合使用不同版本的React是不好的,原因有很多,但从长远地看,我们应该尽可能避免出现这种问题。 (With Hooks, we might need to force deduplication though.)PS: 这段不知道该如何翻译比较好...另一种可行的方案是去判断其原型上是否存在
render
方法。但是当时尚不清楚组件的API会如何发展。而且每次检查都会带来相应的成本,我们不想这样做。而且当render
被定义实例方法,例如使用类属性语法,这同样也会导致判断不准确。所以我们选择了另外一个方案,我们通过添加一个特殊的标志来判断是否是类组件。React通过检查这个特殊标志是否存在来判断当前这个组件是不是类组件。
一开始我们直接将这个特殊标志添加到React本身:
但是一些对class的编译会忽略掉对class静态属性的复制,所以你有可能获取不到这个特殊标志。
这也是为什么React将这个特殊标志转移到
React.Component.prototype
:这就是在这个问题上React做的全部事情。
你可能会疑惑这个特殊标志为什么是一个对象而不是一个布尔值。恩~自己看这个issue,我也不知道怎么翻译,词不达意最要命...
现在React就是使用
isReactComponent
来做是否是类组件的检查。如果你不是声明一个类组件,那么React就不会去查找
isReactComponent
这个属性即我们也不会把它当做一个类组件来处理。现在你可能知道关于“Cannot call a class as a function
”这个问题在stackoverflow中得票最高的答案为什么是add extends React.Component
了吧。当存在render方法却又找不到isReactComponent
属性时会触发警告!你可能会说这看起来像是Bait-and-switch。实际的解决方案非常简单,那为什么我最终会使用这个解决方案当还有其他解决方案可以考虑的时候?
以我的经验来看,这个问题(API的设计及实现,即实现可能不是最优雅的)经常出现在一些第三方库的API设计上。为了考虑如何设计一个易于使用的API,你需要考虑到其语言语义的正确性(可能还涉及到多种语言)、运行时的性能、考虑在有无编译环境下的使用、生态的建设及打包的解决方案、早期的警告提示以及其他需要考虑的东西。最终的结果可能不是最优雅的,但一定是最实用的。
如果一个API的设计是成功的,那么使用它的人就永远不会去考虑这个API是如何实现的。相反,他们会更专注于他们的工作。
但是如果好奇它是怎么工作的,能了解它的实现那是最好不过的事了。