Eine unerwartete Reihenfolge zum Initialisieren geerbter Klassen in JavaScript

Heute hatte ich eine kleine Aufgabe, JS-Code umzugestalten, und ich stieß auf ein unerwartetes Merkmal der Sprache, über das ich über 7 Jahre Programmiererfahrung gesammelt habe von vielen gehasst Sprache hat nicht gedacht und ist nicht rübergekommen.


Außerdem konnte ich weder im russischen noch im englischen Internet etwas finden und beschloss daher, diese nicht sehr lange, nicht die interessanteste, aber nützliche Notiz zu veröffentlichen.


Um die traditionellen und bedeutungslosen foo/bar Konstanten nicht zu verwenden, werde ich direkt auf das Beispiel zeigen, das wir im Projekt hatten, aber immer noch ohne interne Logik und mit falschen Werten. Denken Sie daran, dass sich die Beispiele dennoch als ziemlich synthetisch herausstellten.

Wir treten auf einen Rechen


Wir haben also eine Klasse:


 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 

Alles ist logisch


Und dann mussten wir einen anderen Tooltip erstellen, in dem sich das template ändert


 class SpecialTooltip extends BaseTooltip { template = 'otherTemplate' } 

Und hier erwartete mich eine Überraschung, denn beim Erstellen eines Objekts eines neuen Typs geschieht Folgendes


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

Die BaseTooltip.prototype.template wurde mit dem Wert BaseTooltip.prototype.template , nicht wie erwartet mit BaseTooltip.prototype.template .


Wir treten vorsichtig auf den Rechen, Video aufnehmen


Da Chrome DevTools nicht wissen, wie Klassenfelder zugewiesen werden, müssen Sie auf Tricks zurückgreifen, um zu verstehen, was passiert. Mit einem kleinen Helfer protokollieren wir den Moment der Zuweisung zu einer Variablen


 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 

Und wenn wir diesen Ansatz auf die geerbte Klasse anwenden, erhalten wir Folgendes Seltsames:


 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 

Ich war mir sicher, dass die Felder des Objekts zuerst initialisiert werden und dann der Rest des Konstruktors aufgerufen wird. Es stellt sich heraus, dass alles schwieriger ist.


Wir treten auf den Rechen und malen den Stiel


Wir erschweren die Situation, indem wir dem Konstruktor einen weiteren Parameter hinzufügen, den wir unserem Objekt zuweisen


 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 

Und nur diese Art des Debuggens (naja, keine Warnungen) hat ein wenig geklärt


Woher kam dieses Problem:

Zuvor wurde dieser Code in das Marionette-Framework geschrieben und sah (bedingt) so aus


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

Bei Verwendung von Marionette funktionierte alles wie erwartet, render die render wurde mit dem in der Klasse angegebenen template aufgerufen. Beim Kopieren der Modullogik nach ES6 trat jedoch das im Artikel beschriebene Problem auf


Zähle die Unebenheiten


Das Ergebnis:


Wenn Sie ein Objekt einer geerbten Klasse erstellen, ist die Reihenfolge der Vorgänge wie folgt:


  • Objektfelder aus einer geerbten Klassendeklaration initialisieren
  • Ausführung des Konstruktors der geerbten Klasse (einschließlich Initialisierung der Felder im Konstruktor)
  • Erst nach dieser Initialisierung der Felder des Objekts aus der aktuellen Klasse
  • Ausführen des Konstruktors der aktuellen Klasse

Wir bringen den Rechen in die Scheune zurück


Insbesondere in meiner Situation kann das Problem entweder durch Mixins oder durch Übergeben der Vorlage an den Konstruktor gelöst werden. Wenn die Anwendungslogik jedoch das Überschreiben einer großen Anzahl von Feldern erfordert, wird dies zu einem ziemlich schmutzigen Weg.


Es wäre schön, in den Kommentaren Ihre Vorschläge zur eleganten Lösung des Problems zu lesen.

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


All Articles