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')
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')
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')
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')
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)
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.