Typcript: не может получить доступ к значению элемента в унаследованном конструкторе класса
У меня есть класс A
, а класс B
унаследован от него.
class A {
constructor(){
this.init();
}
init(){}
}
class B extends A {
private myMember = {value:1};
constructor(){
super();
}
init(){
console.log(this.myMember.value);
}
}
const x = new B();
Когда я запускаю этот код, я получаю следующую ошибку:
Uncaught TypeError: Cannot read property 'value' of undefined
Как я могу избежать этой ошибки?
Мне ясно, что код JavaScript вызовет метод init
до создания myMember
, но для его работы должна быть какая-то практика/шаблон.
Ответы
Ответ 1
Вот почему на некоторых языках (cough С#) инструменты анализа кода используют использование виртуальных элементов внутри конструкторов.
В Инициализации поля машинописного ввода происходит в конструкторе после вызова базового конструктора. Тот факт, что инициализация полей написана рядом с полем, является просто синтаксическим сахаром. Если мы посмотрим на сгенерированный код, проблема станет ясна:
function B() {
var _this = _super.call(this) || this; // base call here, field has not been set, init will be called
_this.myMember = { value: 1 }; // field init here
return _this;
}
Вы должны рассмотреть решение, в котором init вызывается извне экземпляра, а не в конструкторе:
class A {
constructor(){
}
init(){}
}
class B extends A {
private myMember = {value:1};
constructor(){
super();
}
init(){
console.log(this.myMember.value);
}
}
const x = new B();
x.init();
Или у вас может быть дополнительный параметр для вашего конструктора, который указывает, следует ли вызывать init
и не вызывать его в производном классе.
class A {
constructor()
constructor(doInit: boolean)
constructor(doInit?: boolean){
if(doInit || true)this.init();
}
init(){}
}
class B extends A {
private myMember = {value:1};
constructor()
constructor(doInit: boolean)
constructor(doInit?: boolean){
super(false);
if(doInit || true)this.init();
}
init(){
console.log(this.myMember.value);
}
}
const x = new B();
Или очень очень грязное решение setTimeout
, которое отложит инициализацию до завершения текущего кадра. Это позволит завершить вызов родительского конструктора, но между вызовом конструктора будет промежуток времени, и когда истечет время ожидания, когда объект не был init
class A {
constructor(){
setTimeout(()=> this.init(), 1);
}
init(){}
}
class B extends A {
private myMember = {value:1};
constructor(){
super();
}
init(){
console.log(this.myMember.value);
}
}
const x = new B();
// x is not yet inited ! but will be soon
Ответ 2
Поскольку свойство myMember
доступно в родительском конструкторе (init()
вызывается во время вызова super()
), нет способа, каким образом его можно определить в дочернем конструкторе, не попав в состояние гонки.
Существует несколько альтернативных подходов.
init
hook
init
считается крючком, который нельзя вызывать в конструкторе класса. Вместо этого он называется явно:
new B();
B.init();
Или он называется неявно каркасом, как часть жизненного цикла приложения.
Статическое свойство
Если свойство должно быть константой, оно может быть статическим свойством.
Это наиболее эффективный способ, потому что для этого нужны статические члены, но синтаксис может быть не столь привлекательным, потому что для этого требуется использовать this.constructor
вместо имени класса, если статическое свойство должно быть правильно указано в дочерних классах:
class B extends A {
static readonly myMember = { value: 1 };
init() {
console.log((this.constructor as typeof B).myMember.value);
}
}
Геттер/сеттер недвижимости
Дескриптор свойства может быть определен в прототипе класса с синтаксисом get
/set
. Если свойство должно быть примитивным постоянным, оно может быть просто геттером:
class B extends A {
get myMember() {
return 1;
}
init() {
console.log(this.myMember);
}
}
Он становится более взломанным, если свойство не является постоянным или примитивным:
class B extends A {
private _myMember?: { value: number };
get myMember() {
if (!('_myMember' in this)) {
this._myMember = { value: 1 };
}
return this._myMember!;
}
set myMember(v) {
this._myMember = v;
}
init() {
console.log(this.myMember.value);
}
}
Инициализация на месте
Свойство может быть инициализировано, когда оно сначала открывается. Если это происходит в методе init
где this
можно получить до конструктора класса B
, это должно произойти там:
class B extends A {
private myMember?: { value: number };
init() {
this.myMember = { value: 1 };
console.log(this.myMember.value);
}
}
Асинхронная инициализация
метод init
может стать асинхронным. Состояние инициализации должно отслеживаться, поэтому класс должен реализовать для этого API, например, на основе обещаний:
class A {
initialization = Promise.resolve();
constructor(){
this.init();
}
init(){}
}
class B extends A {
private myMember = {value:1};
init(){
this.initialization = this.initialization.then(() => {
console.log(this.myMember.value);
});
}
}
const x = new B();
x.initialization.then(() => {
// class is initialized
})
Этот подход может рассматриваться как антипаттерн для данного конкретного случая, поскольку процедура инициализации является синхронной, но может быть подходящей для асинхронных процедур инициализации.
Класс Desugared
Поскольку классы ES6 имеют ограничения на использование this
до super
, дочерний класс может быть освобожден от функции, чтобы избежать этого ограничения:
interface B extends A {}
interface BPrivate extends B {
myMember: { value: number };
}
interface BStatic extends A {
new(): B;
}
const B = <BStatic><Function>function B(this: BPrivate) {
this.myMember = { value: 1 };
return A.call(this);
}
B.prototype.init = function () {
console.log(this.myMember.value);
}
Это редко бывает хорошим вариантом, потому что класс desugared должен быть дополнительно введен в TypeScript. Это также не будет работать с родными родительскими классами (TypeScript es6
и esnext
target).
Ответ 3
Один из подходов, который вы можете использовать, - использовать getter/setter для myMember и управлять значением по умолчанию в getter. Это предотвратит неопределенную проблему и позволит вам сохранить почти ту же структуру, что и у вас. Как это:
class A {
constructor(){
this.init();
}
init(){}
}
class B extends A {
private _myMember;
constructor(){
super();
}
init(){
console.log(this.myMember.value);
}
get myMember() {
return this._myMember || { value: 1 };
}
set myMember(val) {
this._myMember = val;
}
}
const x = new B();
Ответ 4
Попробуй это:
class A {
constructor() {
this.init();
}
init() { }
}
class B extends A {
private myMember = { 'value': 1 };
constructor() {
super();
}
init() {
this.myMember = { 'value': 1 };
console.log(this.myMember.value);
}
}
const x = new B();
Ответ 5
Супер должен быть первой командой. Помните, что машинопись является более "javascript с документацией типов", а не языком сама по себе.
Если вы посмотрите на переписанный код.js, он будет хорошо виден:
class A {
constructor() {
this.init();
}
init() {
}
}
class B extends A {
constructor() {
super();
this.myMember = { value: 1 };
}
init() {
console.log(this.myMember.value);
}
}
const x = new B();
Ответ 6
Вам нужно вызвать init в классе A?
Это прекрасно работает, но я не знаю, есть ли у вас разные требования:
class A {
constructor(){}
init(){}
}
class B extends A {
private myMember = {value:1};
constructor(){
super();
this.init();
}
init(){
console.log(this.myMember.value);
}
}
const x = new B();
Ответ 7
Как это:
class A
{
myMember;
constructor() {
}
show() {
alert(this.myMember.value);
}
}
class B extends A {
public myMember = {value:1};
constructor() {
super();
}
}
const test = new B;
test.show();