tipos datos clases javascript typescript oop inheritance ecmascript-6

javascript - datos - clases en typescript



Typescript: no se puede acceder al valor de miembro en el constructor de clase heredado (7)

Tengo una clase A y una clase B heredada de ella.

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();

Cuando ejecuto este código, obtengo el siguiente error:

Uncaught TypeError: Cannot read property ''value'' of undefined

¿Cómo puedo evitar este error?

Para mí está claro que el código JavaScript llamará al método init antes de crear el myMember , pero debería haber alguna práctica / patrón para que funcione.


¿Tienes que llamar a init en clase A?

Eso funciona bien, pero no sé si tiene diferentes requisitos:

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();


Debido a que se accede a la propiedad myMember en el constructor principal (se llama a init() durante la llamada super() ), no hay forma de definirla en el constructor secundario sin alcanzar una condición de carrera.

Hay varios enfoques alternativos.

gancho de init

init se considera un gancho que no debería llamarse en el constructor de clases. En su lugar, se llama explícitamente:

new B(); B.init();

O es llamado implícitamente por el marco, como parte del ciclo de vida de la aplicación.

Propiedad estática

Si se supone que una propiedad es una constante, puede ser una propiedad estática.

Esta es la forma más eficiente porque es para lo que están los miembros estáticos, pero la sintaxis puede no ser tan atractiva porque requiere usar this.constructor lugar del nombre de la clase si la propiedad estática se debe referir correctamente en las clases secundarias:

class B extends A { static readonly myMember = { value: 1 }; init() { console.log((this.constructor as typeof B).myMember.value); } }

Captador de propiedades

El descriptor de propiedad se puede definir en el prototipo de la clase con la sintaxis get / set . Si se supone que una propiedad es una constante primitiva, puede ser solo un captador:

class B extends A { get myMember() { return 1; } init() { console.log(this.myMember); } }

Se vuelve más intrépido si la propiedad no es constante o primitiva:

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); } }

Inicialización in situ

Una propiedad puede ser inicializada donde se accede primero. Si esto sucede en el método init , al que se puede acceder antes del constructor de la clase B , esto debería ocurrir allí:

class B extends A { private myMember?: { value: number }; init() { this.myMember = { value: 1 }; console.log(this.myMember.value); } }

Inicialización asíncrona

init método init puede volverse asíncrono. El estado de inicialización debe ser rastreable, por lo que la clase debe implementar alguna API para eso, por ejemplo, basada en la promesa:

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 })

Este enfoque puede considerarse antipattern para este caso particular porque la rutina de inicialización es intrínsecamente síncrona, pero puede ser adecuada para las rutinas de inicialización asíncronas.

Clase desugared

Dado que las clases de ES6 tienen limitaciones en el uso de this antes de super , la clase de niño puede desugarse a una función para evadir esta limitación:

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); }

Rara vez es una buena opción, ya que la clase desugared debe escribirse adicionalmente en TypeScript. Esto tampoco funcionará con clases primarias nativas (TypeScript es6 y esnext target).


Es por esto que en algunos idiomas (tos C #), las herramientas de análisis de códigos marcan el uso de miembros virtuales dentro de los constructores.

En el campo mecanografiado, las inicializaciones ocurren en el constructor, después de la llamada al constructor base. El hecho de que las inicializaciones de campo se escriban cerca del campo es solo azúcar sintáctica. Si miramos el código generado, el problema se aclara:

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; }

Debe considerar una solución en la que se llame a init desde fuera de la instancia y no en el constructor:

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();

O puede tener un parámetro adicional para su constructor que especifique si se debe llamar a init y no a la clase derivada también.

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();

O la muy sucia solución de setTimeout , que diferirá la inicialización hasta que se complete el cuadro actual. Esto permitirá que se complete la llamada del constructor principal, pero habrá un interino entre la llamada del constructor y cuando el tiempo de espera expire cuando el objeto no se haya 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


Me gusta esto :

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();


Prueba esto:

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();


Super tiene que ser el primer comando. Recuerde que mecanografiar es más "javascript con documentación de tipos" en lugar de lenguaje por sí solo.

Si miras el código transpilado .js es claramente visible:

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();


Un enfoque que podría tomar es usar un getter / setter para myMember y administrar el valor predeterminado en el getter. Esto evitaría el problema indefinido y le permitiría mantener casi exactamente la misma estructura que tiene. Me gusta esto:

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();