Uma ordem inesperada para inicializar classes herdadas em JavaScript

Hoje, tive uma pequena tarefa de refatorar o código JS e me deparei com um recurso inesperado da linguagem, sobre o qual mais de 7 anos de minha experiência em programação neste odiado por muitos a linguagem não pensou e não se deparou.


Além disso, não consegui encontrar nada na Internet russa ou inglesa e, portanto, decidi publicá-la não por muito tempo, nem a nota mais interessante, mas útil.


Para não usar as constantes foo/bar tradicionais e sem sentido, mostrarei diretamente o exemplo que tivemos no projeto, mas ainda sem um monte de lógica interna e com valores falsos. Lembre-se de que, mesmo assim, os exemplos foram bastante sintéticos.

Nós pisamos em um ancinho


Então nós temos uma classe:


 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 

Tudo é lógico


E então precisamos criar outro tipo de dica de ferramenta na qual o campo do template muda


 class SpecialTooltip extends BaseTooltip { template = 'otherTemplate' } 

E aqui uma surpresa me esperava, porque ao criar um objeto de um novo tipo, acontece o seguinte


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

O método de renderização foi BaseTooltip.prototype.template com o valor BaseTooltip.prototype.template , não SpecialTooltip.prototype.template , como eu esperava.


Nós pisamos no ancinho com cuidado, gravação de vídeo


Como o DevTools do chrome não sabe como atribuir campos de classe, é necessário recorrer a truques para entender o que está acontecendo. Usando um ajudante pequeno, registramos o momento da atribuição em uma variável


 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 

E quando aplicamos essa abordagem à classe herdada, obtemos o seguinte estranho:


 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 

Eu tinha certeza de que os campos do objeto são inicializados primeiro e, em seguida, o restante do construtor é chamado. Acontece que tudo é mais complicado.


Nós pisamos no ancinho, pintando o talo


Nós complicamos a situação adicionando outro parâmetro ao construtor, que atribuímos ao nosso 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 

E somente essa forma de depuração (bem, não alertas) esclareceu um pouco


De onde veio esse problema:

Anteriormente, esse código era escrito na estrutura do Marionette e parecia (condicionalmente) assim


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

Ao usar o Marionette, tudo funcionou como eu esperava, ou seja, o método de render foi chamado com o valor do template especificado na classe, mas ao copiar a lógica do módulo para o ES6, o problema descrito no artigo saiu


Contar os solavancos


O resultado:


Ao criar um objeto de uma classe herdada, a ordem do que está acontecendo é a seguinte:


  • Inicializando campos de objetos a partir de uma declaração de classe herdada
  • Execução do construtor da classe herdada (incluindo inicialização de campos dentro do construtor)
  • Somente após essa inicialização dos campos do objeto da classe atual
  • Executando o construtor da classe atual

Devolvemos o ancinho ao celeiro


Especificamente, na minha situação, o problema pode ser resolvido através de mixins ou passando o modelo para o construtor, mas quando a lógica do aplicativo exige a substituição de um grande número de campos, isso se torna uma maneira bastante suja.


Seria bom ler nos comentários suas sugestões sobre como resolver o problema de maneira elegante.

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


All Articles