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