Un ordre inattendu pour initialiser les classes héritées en JavaScript

Aujourd'hui, j'ai eu une petite tâche de refactorisation du code JS, et je suis tombé sur une caractéristique inattendue du langage, à propos de laquelle plus de 7 ans d'expérience en programmation dans ce détesté par beaucoup la langue ne pensait pas et ne se rencontrait pas.


De plus, je n'ai rien trouvé sur Internet russe ou anglais, et j'ai donc décidé de publier cette note pas très longue, pas la plus intéressante mais utile.


Afin de ne pas utiliser les constantes foo/bar traditionnelles et dénuées de sens, je vais montrer directement sur l'exemple que nous avions dans le projet, mais toujours sans tas de logique interne et avec de fausses valeurs. N'oubliez pas que les exemples se sont tout de même avérés assez synthétiques.

On marche sur un râteau


Nous avons donc une 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 

Tout est logique


Et puis nous avons dû créer un autre type d'infobulle dans laquelle le champ du template change


 class SpecialTooltip extends BaseTooltip { template = 'otherTemplate' } 

Et ici, une surprise m'attendait, car lors de la création d'un objet d'un nouveau type, les événements suivants se produisent


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

La méthode de rendu a été BaseTooltip.prototype.template avec la valeur BaseTooltip.prototype.template , pas SpecialTooltip.prototype.template , comme je m'y attendais.


Nous montons le râteau avec précaution, tournage vidéo


Étant donné que Chrome DevTools ne sait pas comment attribuer des champs de classe, vous devez recourir à des astuces pour comprendre ce qui se passe. À l'aide d'un petit assistant, nous enregistrons le moment de l'affectation à une 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 

Et lorsque nous appliquons cette approche à la classe héritée, nous obtenons l'étrange suivant:


 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 

J'étais sûr que les champs de l'objet sont initialisés en premier, puis le reste du constructeur est appelé. Il s'avère que tout est plus délicat.


Nous marchons sur le râteau, peignant la tige


Nous compliquons la situation en ajoutant un autre paramètre au constructeur, que nous attribuons à notre objet


 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 

Et seule cette façon de déboguer (enfin, pas les alertes) a un peu clarifié


D'où vient ce problème:

Auparavant, ce code était écrit sur le framework Marionette et ressemblait (conditionnellement) à ceci


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

Lors de l'utilisation de Marionette, tout a fonctionné comme prévu, c'est-à-dire que la méthode de render été appelée avec la valeur de template spécifiée dans la classe, mais lors de la copie de la logique du module vers ES6, le problème décrit dans l'article est sorti.


Comptez les bosses


Le résultat:


Lors de la création d'un objet d'une classe héritée, l'ordre de ce qui se passe est le suivant:


  • Initialisation des champs d'objet à partir d'une déclaration de classe héritée
  • Exécution du constructeur de la classe héritée (y compris initialisation des champs à l'intérieur du constructeur)
  • Seulement après cette initialisation des champs de l'objet de la classe courante
  • Exécution du constructeur de la classe actuelle

Nous retournons le râteau à la grange


Plus précisément, dans ma situation, le problème peut être résolu soit par le biais de mixins, soit en passant le modèle au constructeur, mais lorsque la logique d'application nécessite de remplacer un grand nombre de champs, cela devient un moyen plutôt sale.


Ce serait bien de lire dans les commentaires vos suggestions sur la façon de résoudre le problème avec élégance.

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


All Articles