Ответ 1
Не ошибка
Во-первых, это не ошибка в TypeScript, Babel или во время выполнения JS.
Почему это должно быть так
Первое, что вы можете сделать, это "Почему бы не сделать это правильно!?!?". Давайте рассмотрим конкретный случай испускания TypeScript. Фактический ответ зависит от того, какую версию ECMAScript мы испускаем для кода класса.
Испускание нижнего уровня: ES3/ES5
Давайте рассмотрим код, испускаемый TypeScript для ES3 или ES5. Я упростил + немного пояснил это для удобства чтения:
var Base = (function () {
function Base() {
// BASE CLASS PROPERTY INITIALIZERS
this.myColor = 'blue';
console.log(this.myColor);
}
return Base;
}());
var Derived = (function (_super) {
__extends(Derived, _super);
function Derived() {
// RUN THE BASE CLASS CTOR
_super();
// DERIVED CLASS PROPERTY INITIALIZERS
this.myColor = 'red';
// Code in the derived class ctor body would appear here
}
return Derived;
}(Base));
Базовый класс испускает неоспоримое правильно - поля инициализируются, затем запускает тело конструктора. Вы, конечно, не хотели бы обратного - инициализация полей перед запуском тела конструктора означала бы, что вы не могли видеть значения полей до тех пор, пока конструктор, который не тот, кто хочет.
Исправлен ли производный класс?
Нет, вы должны поменять порядок
Многие люди утверждают, что излучение производного класса должно выглядеть так:
// DERIVED CLASS PROPERTY INITIALIZERS
this.myColor = 'red';
// RUN THE BASE CLASS CTOR
_super();
Это очень неправильно по ряду причин:
- Он не имеет соответствующего поведения в ES6 (см. Следующий раздел)
- Значение
'red'
дляmyColor
будет немедленно перезаписано значением базового класса "синий", - Инициализатор производного класса может вызывать методы базового класса, которые зависят от инициализации базового класса.
В этом последнем пункте рассмотрим этот код:
class Base {
thing = 'ok';
getThing() { return this.thing; }
}
class Derived extends Base {
something = this.getThing();
}
Если инициализаторы производного класса выполнялись до инициализаторов базового класса, Derived#something
то всегда было бы undefined
, когда ясно, что это должно быть 'ok'
.
Нет, вы должны использовать машину времени
Многие другие люди утверждали бы, что нужно делать что-то другое, чтобы Base
знала, что Derived
имеет инициализатор поля.
Вы можете написать примеры решений, которые зависят от знания всего юниверса кода, который будет запущен. Но TypeScript/Babel/etc не может гарантировать, что это существует. Например, Base
может быть в отдельном файле, где мы не можем увидеть его реализацию.
Испускание нижнего уровня: ES6
Если вы этого еще не знали, пришло время узнать: классы не являются функцией TypeScript. Они являются частью ES6 и имеют определенную семантику. Но классы ES6 не поддерживают инициализаторы полей, поэтому они преобразуются в ES6-совместимый код. Это выглядит так:
class Base {
constructor() {
// Default value
this.myColor = 'blue';
console.log(this.myColor);
}
}
class Derived extends Base {
constructor() {
super(...arguments);
this.myColor = 'red';
}
}
Вместо
super(...arguments);
this.myColor = 'red';
Должны ли мы это сделать?
this.myColor = 'red';
super(...arguments);
Нет, потому что это не работает. Нельзя ссылаться на this
прежде чем ссылаться на super
в производном классе. Он просто не может работать таким образом.
ES7+: публичные поля
Комитет TC39, который контролирует JavaScript, исследует добавление инициализаторов полей в будущую версию языка.
Вы можете прочитать об этом в GitHub или прочитать конкретную проблему с порядком инициализации.
Обновление ООП: виртуальное поведение от конструкторов
Все языки ООП имеют общие руководящие принципы, некоторые из которых четко сформулированы, некоторые из них неявно по соглашению:
Не вызывайте виртуальные методы из конструктора
Примеры:
- С# Виртуальный вызов участника в конструкторе
- C++ Вызов виртуальных функций внутри конструкторов
- Python Вызов функций-членов из конструктора
- Java Является ли в порядке вызова абстрактного метода из конструктора в Java?
В JavaScript мы должны немного расширить это правило
Не наблюдайте виртуальное поведение от конструктора
а также
Инициализация свойства класса считается виртуальной
Решения
Стандартное решение состоит в том, чтобы преобразовать инициализацию поля в параметр конструктора:
class Base {
myColor: string;
constructor(color: string = "blue") {
this.myColor = color;
console.log(this.myColor);
}
}
class Derived extends Base {
constructor() {
super("red");
}
}
// Prints "red" as expected
const x = new Derived();
Вы также можете использовать шаблон init
, хотя вам нужно быть осторожным, чтобы не наблюдать за ним виртуальное поведение и не делать ничего в производном методе init
который требует полной инициализации базового класса:
class Base {
myColor: string;
constructor() {
this.init();
console.log(this.myColor);
}
init() {
this.myColor = "blue";
}
}
class Derived extends Base {
init() {
super.init();
this.myColor = "red";
}
}
// Prints "red" as expected
const x = new Derived();