为什么我们要写 super(props)?
November 30, 2018
我听说 Hooks 现在非常火。不过有趣的是,我想用 class 组件的一些冷知识来开启这篇博客。怎么样,是不是很反差!
这些小坑对于高效使用 React 并不重要。但如果你喜欢深挖底层原理,或许会觉得它们很有趣。
先来第一个。
我写过的 super(props)
次数多到不敢细数:
class Checkbox extends React.Component {
constructor(props) {
super(props);
this.state = { isOn: true };
}
// ...
}
当然,类字段提案让我们可以省去这些繁琐的写法:
class Checkbox extends React.Component {
state = { isOn: true };
// ...
}
React 0.13 在 2015 年支持普通类时,就计划引入这样的语法。定义 constructor
并调用 super(props)
,本来就是在类字段语法普及前的临时方案。
不过我们还是回到只用 ES2015 特性的这个例子:
class Checkbox extends React.Component {
constructor(props) {
super(props);
this.state = { isOn: true };
}
// ...
}
为什么要调用 super
?能不能不调用?如果必须调用,不传 props
会怎样?还能传别的参数吗? 我们来一一解答。
在 JavaScript 中,super
指的是父类的构造函数。(在我们的例子里,就是 React.Component
的实现。)
需要注意的是,在构造函数里,只有在调用了父类构造函数之后,才能使用 this
。JavaScript 不会让你这么做:
class Checkbox extends React.Component {
constructor(props) {
// 🔴 这里还不能用 `this`
super(props);
// ✅ 现在可以用了
this.state = { isOn: true };
}
// ...
}
JavaScript 强制要求先执行父类构造函数再使用 this
,是有充分理由的。想象一下这样的类继承关系:
class Person {
constructor(name) {
this.name = name;
}
}
class PolitePerson extends Person {
constructor(name) {
this.greetColleagues(); // 🔴 这样写是不允许的,原因见下文
super(name);
}
greetColleagues() {
alert('Good morning folks!');
}
}
假如允许在 super
之前使用 this
,一个月后你可能会把 greetColleagues
改成这样:
greetColleagues() {
alert('Good morning folks!');
alert('My name is ' + this.name + ', nice to meet you!');
}
但你忘了 this.greetColleagues()
是在 super()
之前调用的,这时 this.name
还没被赋值!所以 this.name
根本还没定义!可见,这样的代码非常容易出错且难以理解。
为了避免这种坑,JavaScript 规定:如果你想在构造函数里用 this
,必须先调用 super
。 让父类先把该做的事做完!这个限制同样适用于用类定义的 React 组件:
constructor(props) {
super(props);
// ✅ 现在可以安全使用 `this`
this.state = { isOn: true };
}
那接下来的问题是:为什么要传 props
?
你可能觉得把 props
传给 super
是为了让基类 React.Component
的构造函数初始化 this.props
:
// React 内部实现
class Component {
constructor(props) {
this.props = props;
// ...
}
}
这确实基本没错 —— 实际上,React 就是这么做的。
但有意思的是,即使你调用 super()
时没传 props
,你依然能在 render
和其他方法里访问到 this.props
。(不信你可以自己试试!)
这是怎么做到的呢?其实,React 在调用你的构造函数后,会再次把 props
赋值给实例:
// React 内部
const instance = new YourComponent(props);
instance.props = props;
所以即使你忘了把 props
传给 super()
,React 也会在后面帮你设置。这么做是有原因的。
React 在支持类组件时,并不只是支持 ES6 的类。它的目标是兼容尽可能多的类抽象方式。当时还不确定 ClojureScript、CoffeeScript、ES6、Fable、Scala.js、TypeScript 等等这些方案哪一个会流行。所以 React 故意不强制要求必须调用 super()
—— 虽然 ES6 类是强制的。
那是不是意味着你可以直接写 super()
而不是 super(props)
?
其实最好不要,因为这会让人困惑。 虽然 React 会在你的构造函数执行完后再赋值 this.props
,但在 super
调用和构造函数结束之间,this.props
依然是 undefined:
// React 内部
class Component {
constructor(props) {
this.props = props;
// ...
}
}
// 你的代码
class Button extends React.Component {
constructor(props) {
super(); // 😬 忘记传 props 了
console.log(props); // ✅ {}
console.log(this.props); // 😬 undefined
}
// ...
}
如果在构造函数里调用了其他方法,这种问题会更难排查。所以我建议始终传递 super(props)
,虽然它不是绝对必要的:
class Button extends React.Component {
constructor(props) {
super(props); // ✅ 传递了 props
console.log(props); // ✅ {}
console.log(this.props); // ✅ {}
}
// ...
}
这样可以确保 this.props
在构造函数还没结束前就已经被赋值。
还有最后一个点,老 React 用户可能会好奇。
你可能注意到,在类组件中使用 Context API(无论是老的 contextTypes
还是 React 16.6 新增的 contextType
),context
会作为构造函数的第二个参数传入。
那为什么我们不写 super(props, context)
呢?其实可以,只不过 context 用得没那么多,所以这个坑也就不常见。
有了类字段提案,这个问题基本也就消失了。 没有显式构造函数时,所有参数都会自动传递。这也是为什么像 state = {}
这样的写法可以在需要时引用 this.props
或 this.context
。
而有了 Hooks,我们甚至没有 super
或 this
了。但这是另一个话题了。