Un pedido inesperado para inicializar clases heredadas en JavaScript

Hoy tuve una pequeña tarea de refactorizar el código JS, y me encontré con una característica inesperada del lenguaje, sobre la cual más de 7 años de mi experiencia en programación en este odiado por muchos el lenguaje no pensó y no se encontró.


Además, no pude encontrar nada en Internet en ruso o en inglés, y por lo tanto decidí publicar esto no muy largo, no es la nota más interesante, pero útil.


Para no usar las constantes foo/bar tradicionales y sin sentido, mostraré directamente en el ejemplo que teníamos en el proyecto, pero aún sin un montón de lógica interna y con valores falsos. Recuerde que de todos modos, los ejemplos resultaron ser bastante sintéticos.

Pisamos un rastrillo


Entonces tenemos una clase:


 class BaseTooltip { template = 'baseTemplate' constructor(content) { this.render(content) } render(content) { console.log('render:', content, this.template) } } const tooltip = new BaseTooltip('content') // render: content baseTemplate 

Todo es logico


Y luego necesitábamos crear otro tipo de información sobre herramientas en la que el campo de template cambia


 class SpecialTooltip extends BaseTooltip { template = 'otherTemplate' } 

Y aquí me esperaba una sorpresa, porque al crear un objeto de un nuevo tipo, sucede lo siguiente


 const specialTooltip = new SpecialTooltip('otherContent') // render: otherContent baseTemplate // ^  

El método de renderizado se BaseTooltip.prototype.template con el valor BaseTooltip.prototype.template , no SpecialTooltip.prototype.template , como esperaba.


Pisamos el rastrillo con cuidado, filmando video


Dado que las DevTools de Chrome no saben cómo asignar campos de clase, debe recurrir a trucos para comprender lo que está sucediendo. Usando un pequeño ayudante, registramos el momento de asignación a una variable


 function logAndReturn(value) { console.log(`set property=${value}`) return value } class BaseTooltip { template = logAndReturn('baseTemplate') constructor(content) { console.log(`call constructor with property=${this.template}`) this.render(content) } render(content) { console.log(content, this.template) } } const tooltip = new BaseTooltip('content') // set property=baseTemplate // called constructor BaseTooltip with property=baseTemplate // render: content baseTemplate 

Y cuando aplicamos este enfoque a la clase heredada, obtenemos lo siguiente extraño:


 class SpecialTooltip extends BaseTooltip { template = logAndReturn('otherTemplate') } const tooltip = new SpecialTooltip('content') // set property=baseTemplate // called constructor SpecialTooltip with property=baseTemplate // render: content baseTemplate // set property=otherTemplate 

Estaba seguro de que los campos del objeto se inicializan primero, y luego se llama al resto del constructor. Resulta que todo es más complicado.


Pisamos el rastrillo, pintamos el tallo


Complicamos la situación agregando otro parámetro al constructor, que asignamos a nuestro objeto


 class BaseTooltip { template = logAndReturn('baseTemplate') constructor(content, options) { this.options = logAndReturn(options) // <---   console.log(`called constructor ${this.constructor.name} with property=${this.template}`) this.render(content) } render(content) { console.log(content, this.template, this.options) // <---   } } class SpecialTooltip extends BaseTooltip { template = logAndReturn('otherTemplate') } const tooltip = new SpecialTooltip('content', 'someOptions') //    : // set property=baseTemplate // set property=someOptions // called constructor SpecialTooltip with property=baseTemplate // render: content baseTemplate someOptions // set property=otherTemplate 

Y solo esta forma de depuración (bueno, no alertas) se aclaró un poco


¿De dónde vino este problema?

Anteriormente, este código se escribió en el marco de Marionette y se veía (condicionalmente) así


 const BaseTooltip = Marionette.Object.extend({ template: 'baseTemplate', initialize(content) { this.render(content) }, render(content) { console.log(content, this.template) }, }) const SpecialTooltip = BaseTooltip.extend({ template: 'otherTemplate' }) 

Al usar Marionette, todo funcionó como esperaba, es decir, se llamó al método de render con el valor de template especificado en la clase, pero al copiar la lógica del módulo a ES6, el problema descrito en el artículo salió


Cuenta los golpes


El resultado:


Al crear un objeto de una clase heredada, el orden de lo que sucede es el siguiente:


  • Inicializando campos de objeto desde una declaración de clase heredada
  • Ejecución del constructor de la clase heredada (incluida la inicialización de campos dentro del constructor)
  • Solo después de esta inicialización de los campos del objeto de la clase actual
  • Ejecutando el constructor de la clase actual

Devolvemos el rastrillo al granero.


Específicamente, en mi situación, el problema se puede resolver a través de mixins o pasando la plantilla al constructor, pero cuando la lógica de la aplicación requiere anular una gran cantidad de campos, esto se convierte en una forma bastante sucia.


Sería bueno leer en los comentarios sus sugerencias sobre cómo resolver el problema con elegancia.

Source: https://habr.com/ru/post/461399/


All Articles