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')
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')
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')
E quando aplicamos essa abordagem à classe herdada, obtemos o seguinte estranho:
class SpecialTooltip extends BaseTooltip { template = logAndReturn('otherTemplate') } const tooltip = new SpecialTooltip('content')
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)
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.