overreactedby Dan Abramov

为什么我们要写 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.propsthis.context

而有了 Hooks,我们甚至没有 superthis 了。但这是另一个话题了。

Pay what you like

Edit on GitHub