Я писал super(props) большое количество раз в жизни, и хотел бы знать:
class Checkbox extends React.Component {
  constructor(props) {
    super(props);
    this.state = { isOn: true };
  }
  // ...
}

Конечно, class fields proposal позволяет нам пропустить церемонию инициализации:
class Checkbox extends React.Component {
  state = { isOn: true };
  // ...
}

Подобный синтаксис был запланирован, когда в React 0.13 была добавлена ​​поддержка простых классов в 2015 году. Определение конструктора и вызов super(props) всегда предполагалось как временное решение, пока поля класса не предоставят эргономичную альтернативу.

Но вернемся к этому примеру, используя только функции ES2015:
class Checkbox extends React.Component {
  constructor(props) {
    super(props);
    this.state = { isOn: true };
  }
  // ...
}

Здесь у вас не избежно должны возникнуть следующие вопросы: Почему мы вызываем super? Можем ли мы не вызывать его? Если нам нужно вызвать его, что произойдет, если мы не передадим props? Есть ли другие аргументы?

В JavaScript super относится к конструктору родительского класса. (В нашем примере это указывает на реализацию React.Component.)
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!');
  }
}

Представьте, что вы использовали this до того, как был разрешен вызов super. Месяц спустя мы могли бы изменить welcomeColleagues, чтобы включить имя человека в сообщение:
 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:

Действительно, так оно и есть. Это то что он делает.

Но каким-то образом, даже если вы вызовете super() без аргумента props, вы все равно сможете получить доступ к 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 после вызова вашего конструктора. Но this.props по-прежнему будет undefined между вызовом super и концом вашего конструктора:
// Inside React
class Component {
  constructor(props) {
    this.props = props;
    // ...
  }
}

// Inside your code
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, либо с современным contextType API, добавленным в React 16.6), контекст передается в качестве второго аргумента конструктору.

Так почему бы нам вместо этого не написать super(props, context)? Ну мы конечно же могли бы, но контекст используется редко, так что эта ловушка не так часто всплывает.

С предложением о полях классов вся эта ловушка в основном исчезает. Без явного конструктора все аргументы передаются автоматически. Именно это позволяет такому выражению, как state = {}, при необходимости включать ссылки на this.props или this.context.

С хуками у нас даже нет super или this. Но в прочем это уже совсем другая история…

Комментарии (5)


  1. Aquahawk
    01.02.2022 12:50

    А я в упор не понимаю этой истерии с запретом доступа к this до вызова super().

    Всегда можно налететь на такой кейс

    class A {
        constructor(param: string) {
            this.a = param;
            this.test();
        }
        public a : string;
        public test() {
            console.log(this.a)
        }
    }
    
    class B extends A {
        constructor(param1: string, param2:string) {
            super(param1)
            this.b = param2;
        }
        public b : string;
        public test() {
            console.log(this.a + ' ' + this.b);
        }
    }
    
    new B('foo', 'bar'); // "foo undefined" 

    Потыкать плейграунд

    При этом если заткнуть тайпскрипт варнинг и таргетиться в es5 всё будет прекрасно работать. Нет никакого технического ограничения почему нужно это запрещать. Имхо это работа линтера и детект такого кода должен быть исключительно опционален. И я опечален тем что такие искуственные и неоднозначные ограничения тащат в язык.


  1. nin-jin
    01.02.2022 12:52
    +8

    super надо вызывать по другой причине. До его вызова объекта просто не существует. А после вызова this будет указывать на созвращённый родительским конструктором объект.

    class A {
        constructor() {
            return { foo: 'bar' }
        }
    }
    
    class B extends A {
        constructor() {
            super()
            console.log( this.foo ) // bar
        }
    }


    1. Aquahawk
      01.02.2022 15:13

      Спасибо за пример, запамятовал что так можно. Хотя именно это в JS колассах я бы как раз запретил ибо всё ооп ломается и прототип кривой присваивается если руками возвращаешь другой объект, ибо прототип был прописан до того как отдать объект в конструктор


      1. nin-jin
        01.02.2022 15:25
        +2

        Это не просто так ввели, а чтобы можно было расширять классы хоста типа дом элементов. То есть сначала создаётся хостовой объект, к нему js-биндинг, а потом уже этот биндинг донастраивается в дочерних конструкторах.


    1. Deosis
      02.02.2022 07:48
      +2

      А после вызова this будет указывать на созвращённый родительским конструктором объект.

      В этом весь JavaScript