So funktioniert JS: Klassen und Vererbung, Transpilation in Babel und TypeScript

Klassen sind heutzutage eine der beliebtesten Möglichkeiten, Softwareprojekte zu strukturieren. Dieser Programmieransatz wird auch in JavaScript verwendet. Heute veröffentlichen wir eine Übersetzung von Teil 15 der JS-Ökosystemreihe. In diesem Artikel werden verschiedene Ansätze zum Implementieren von Klassen in JavaScript, Vererbungsmechanismen und Transpiration erläutert. Zunächst erklären wir Ihnen, wie Prototypen funktionieren, und analysieren verschiedene Möglichkeiten, um die klassenbasierte Vererbung in gängigen Bibliotheken zu simulieren. Als nächstes werden wir darüber sprechen, wie es dank der Transpilation möglich ist, JS-Programme zu schreiben, die Funktionen verwenden, die entweder nicht in der Sprache verfügbar sind oder, obwohl sie in Form neuer Standards oder Vorschläge existieren, die sich in verschiedenen Genehmigungsphasen befinden, noch nicht in JS implementiert sind. Motoren. Insbesondere werden wir über Babel- und TypeScript- sowie ECMAScript 2015-Klassen sprechen. Anschließend werden einige Beispiele betrachtet, die die Funktionen der internen Implementierung von Klassen in der V8-JS-Engine demonstrieren.
Bild


Rückblick


In JavaScript werden wir ständig mit Objekten konfrontiert, auch wenn es den Anschein hat, dass wir mit primitiven Datentypen arbeiten. Erstellen Sie beispielsweise ein Zeichenfolgenliteral:

const name = "SessionStack"; 

Danach können wir uns sofort an name wenden, um verschiedene Methoden eines Objekts vom Typ String aufzurufen, in das das von uns erstellte String-Literal automatisch konvertiert wird.

 console.log(name.repeat(2)); // SessionStackSessionStack console.log(name.toLowerCase()); // sessionstack 

Im Gegensatz zu anderen Sprachen können wir in JavaScript durch Erstellen einer Variablen, die beispielsweise eine Zeichenfolge oder eine Zahl enthält, ohne eine explizite Konvertierung mit dieser Variablen arbeiten, als ob sie ursprünglich mit dem new Schlüsselwort und dem entsprechenden Konstruktor erstellt worden wäre. Aufgrund der automatischen Erstellung von Objekten, die primitive Werte einkapseln, können Sie daher mit solchen Werten arbeiten, als wären sie Objekte, insbesondere beziehen Sie sich auf deren Methoden und Eigenschaften.

Eine weitere bemerkenswerte Tatsache in Bezug auf das JavaScript-Typsystem ist, dass beispielsweise Arrays auch Objekte sind. Wenn Sie sich die Ausgabe des Befehls typeof ansehen, der für das Array typeof , können Sie feststellen, dass die untersuchte Entität den object . Als Ergebnis stellt sich heraus, dass die Indizes der Elemente des Arrays nur Eigenschaften eines bestimmten Objekts sind. Wenn wir also über einen Index auf ein Element eines Arrays zugreifen, müssen wir mit einer Eigenschaft eines Objekts vom Typ Array und den Wert dieser Eigenschaft abrufen. Wenn wir darüber sprechen, wie Daten in gewöhnlichen Objekten und Arrays gespeichert werden, führen die folgenden zwei Konstruktionen zur Erstellung nahezu identischer Datenstrukturen:

 let names = ["SessionStack"]; let names = { "0": "SessionStack", "length": 1 } 

Infolgedessen wird der Zugriff auf die Elemente des Arrays und auf die Eigenschaften des Objekts mit derselben Geschwindigkeit ausgeführt. Der Autor dieses Artikels sagt, er habe es bei der Lösung eines komplexen Problems herausgefunden. Einmal musste er nämlich eine ernsthafte Optimierung eines sehr wichtigen Codeteils im Projekt durchführen. Nachdem er viele einfache Ansätze ausprobiert hatte, beschloss er, alle in diesem Code verwendeten Objekte durch Arrays zu ersetzen. Theoretisch ist der Zugriff auf Array-Elemente schneller als die Arbeit mit Hash-Tabellenschlüsseln. Zu seiner Überraschung hatte diese Ersetzung keinerlei Auswirkungen auf die Leistung, da die Arbeit mit Arrays und die Arbeit mit Objekten in JavaScript auf die Interaktion mit Hash-Tabellenschlüsseln zurückzuführen ist, was in beiden Fällen dieselbe Zeit erfordert.

Klassen mit Prototypen simulieren


Wenn wir an Objekte denken, fallen uns als erstes die Klassen ein. Vielleicht hat jeder, der sich heute mit Programmierung beschäftigt, Anwendungen erstellt, deren Struktur auf Klassen und den Beziehungen zwischen ihnen basiert. Obwohl Objekte in JavaScript buchstäblich überall zu finden sind, verwendet die Sprache kein traditionelles klassenbasiertes Vererbungssystem. JavaScript verwendet Prototypen , um ähnliche Probleme zu lösen.


Objekt und sein Prototyp

In JavaScript ist jedes Objekt einem anderen Objekt zugeordnet - mit einem eigenen Prototyp. Wenn Sie versuchen, auf eine Eigenschaft oder Methode eines Objekts zuzugreifen, wird die Suche nach dem, was Sie benötigen, zuerst im Objekt selbst durchgeführt. Wenn die Suche nicht erfolgreich ist, wird sie im Prototyp des Objekts fortgesetzt.

Stellen Sie sich ein einfaches Beispiel vor, das eine Konstruktorfunktion für die Component Basisklasse beschreibt:

 function Component(content) { this.content = content; } Component.prototype.render = function() {   console.log(this.content); } 

Hier weisen wir der Prototypmethode die Funktion render() zu, da wir jede Instanz der Component Klasse benötigen, um diese Methode zu verwenden. Wenn in einer Instanz von Component die render aufgerufen wird, beginnt ihre Suche in dem Objekt selbst, für das sie aufgerufen wird. Anschließend wird die Suche im Prototyp fortgesetzt, wo das System diese Methode findet.


Prototyp und zwei Instanzen der Component-Klasse

Versuchen wir nun, die Component Klasse zu erweitern. Erstellen wir einen Konstruktor für eine neue Klasse - InputField :

 function InputField(value) {   this.content = `<input type="text" value="${value}" />`; } 

Wenn wir die InputField Klasse benötigen InputField um die Funktionalität der Component Klasse zu erweitern und ihre render Methode aufrufen zu können, müssen wir ihren Prototyp ändern. Wenn eine Methode für eine Instanz einer untergeordneten Klasse aufgerufen wird, ist es nicht sinnvoll, sie in einem leeren Prototyp zu suchen. Bei der Suche nach dieser Methode müssen wir in der Component Klasse gefunden werden. Daher müssen wir Folgendes tun:

 InputField.prototype = Object.create(new Component()); 

Wenn Sie nun mit einer Instanz der InputField Klasse arbeiten und die Methode der Component Klasse aufrufen, befindet sich diese Methode im Prototyp der Component Klasse. Um das Vererbungssystem zu implementieren, müssen Sie den InputField Prototyp mit einer Instanz der Component Klasse verbinden. Viele Bibliotheken verwenden Object.setPrototypeOf () , um dieses Problem zu lösen.


Erweitern der Komponentenklasse mit der InputField-Klasse

Die oben genannten Maßnahmen reichen jedoch nicht aus, um einen Mechanismus zu implementieren, der der herkömmlichen Vererbung ähnelt. Jedes Mal, wenn wir die Klasse erweitern, müssen wir die folgenden Aktionen ausführen:

  • Machen Sie den Prototyp der untergeordneten Klasse zu einer Instanz der übergeordneten Klasse.
  • Rufen Sie im Konstruktor der untergeordneten Klasse den Konstruktor der übergeordneten Klasse auf, um sicherzustellen, dass die übergeordnete Klasse korrekt initialisiert ist.
  • Stellen Sie einen Mechanismus zum Aufrufen von Methoden der übergeordneten Klasse in Situationen bereit, in denen die untergeordnete Klasse die übergeordnete Methode überschreibt, die ursprüngliche Implementierung dieser Methode jedoch von der übergeordneten Klasse aufgerufen werden muss.

Wie Sie sehen können, muss ein JS-Entwickler die oben genannten Schritte ständig ausführen, wenn er die Funktionen der klassenbasierten Vererbung nutzen möchte. Für den Fall, dass Sie viele Klassen erstellen müssen, kann dies alles in Form von Funktionen erkannt werden, die zur Wiederverwendung geeignet sind.

Tatsächlich wurde die Aufgabe, die Vererbung anhand von Klassen zu organisieren, in der Praxis der JS-Entwicklung zunächst auf diese Weise gelöst. Insbesondere unter Verwendung verschiedener Bibliotheken. Solche Lösungen wurden sehr beliebt, was eindeutig darauf hinwies, dass in JavaScript eindeutig etwas fehlte. Aus diesem Grund wurden in ECMAScript 2015 neue syntaktische Konstruktionen eingeführt, die die Arbeit mit Klassen unterstützen und die entsprechenden Vererbungsmechanismen implementieren sollen.

Klassentranspilation


Nachdem die neuen Funktionen von ECMAScript 2015 (ES6) vorgeschlagen wurden, wollte die JS-Community sie so schnell wie möglich nutzen, ohne auf den Abschluss des langen Prozesses warten zu müssen, in dem diese Funktionen in JS-Engines und Browsern unterstützt werden. Bei der Lösung solcher Probleme ist die Transpilation gut. In diesem Fall reduziert sich die Kompilierung darauf, den nach den Regeln von ES6 geschriebenen JS-Code in eine Ansicht umzuwandeln, die für Browser verständlich ist, die bisher keine ES6-Funktionen unterstützen. Auf diese Weise wird es beispielsweise möglich, Klassen zu deklarieren und klassenbasierte Vererbungsmechanismen gemäß ES6-Regeln zu implementieren und diese Konstrukte in Code zu konvertieren, der in jedem Browser funktioniert. Schematisch kann dieser Prozess am Beispiel der Verarbeitung einer Pfeilfunktion durch einen Transpiler (eine weitere neue Sprachfunktion, deren Unterstützung Zeit benötigt) wie in der folgenden Abbildung dargestellt dargestellt werden.


Transpilation

Einer der beliebtesten JavaScript-Transpiler ist Babel.js. Lassen Sie uns sehen, wie es funktioniert, indem Sie eine Kompilierung des Component Klassendeklarationscodes durchführen, über den wir oben gesprochen haben. Hier ist also der ES6-Code:

 class Component { constructor(content) {   this.content = content; } render() { console.log(this.content) } } const component = new Component('SessionStack'); component.render(); 

Und hier ist, was dieser Code nach der Transpilation wird:

 var Component = function () { function Component(content) {   _classCallCheck(this, Component);   this.content = content; } _createClass(Component, [{   key: 'render',   value: function render() {     console.log(this.content);   } }]); return Component; }(); 

Wie Sie sehen können, wird ECMAScript 5-Code am Ausgang des Transpilers abgerufen, der in jeder Umgebung ausgeführt werden kann. Außerdem werden hier Aufrufe einiger Funktionen hinzugefügt, die Teil der Babel-Standardbibliothek sind.

Wir sprechen über die Funktionen _classCallCheck() und _createClass() , die im transpilierten Code enthalten sind. Die erste Funktion, _classCallCheck() , soll verhindern, dass die Konstruktorfunktion wie eine reguläre Funktion aufgerufen wird. Dazu wird geprüft, ob der Kontext, in dem die Funktion aufgerufen wird, der Instanzkontext der Component Klasse ist. Der Code prüft, ob das Schlüsselwort this auf eine ähnliche Instanz verweist. Die zweite Funktion, _createClass() , erstellt Objekteigenschaften, die als Array von Objekten übergeben werden, die Schlüssel und ihre Werte enthalten.

Um zu verstehen, wie Vererbung funktioniert, analysieren wir die InputField Klasse, die der Nachkomme der Component Klasse ist. So kommen Klassenbeziehungen in ES6 zusammen:

 class InputField extends Component {   constructor(value) {       const content = `<input type="text" value="${value}" />`;       super(content);   } } 

Hier ist das Ergebnis der Transpilation dieses Codes mit Babel:

 var InputField = function (_Component) { _inherits(InputField, _Component); function InputField(value) {   _classCallCheck(this, InputField);   var content = '<input type="text" value="' + value + '" />';   return _possibleConstructorReturn(this, (InputField.__proto__ || Object.getPrototypeOf(InputField)).call(this, content)); } return InputField; }(Component); 

In diesem Beispiel wird die Logik der Vererbungsmechanismen in einem Aufruf der Funktion _inherits() gekapselt. Es führt dieselben Aktionen aus, die wir oben beschrieben haben, insbesondere mit dem Schreiben einer Instanz der übergeordneten Klasse in den Prototyp der untergeordneten Klasse.

Um den Code zu transponieren, führt Babel mehrere seiner Transformationen durch. Zunächst wird der ES6-Code analysiert und in eine Zwischendarstellung konvertiert, die als abstrakter Syntaxbaum bezeichnet wird . Dann wird der resultierende abstrakte Syntaxbaum in einen anderen Baum konvertiert, von dem jeder Knoten in sein ES5-Äquivalent transformiert wird. Infolgedessen wird dieser Baum in JS-Code konvertiert.

Abstrakter Syntaxbaum in Babel


Ein abstrakter Syntaxbaum enthält Knoten, von denen jeder nur einen übergeordneten Knoten hat. Babel hat einen Basistyp für Knoten. Es enthält Informationen darüber, was der Knoten ist und wo er im Code zu finden ist. Es gibt verschiedene Arten von Knoten, z. B. Knoten zur Darstellung von Literalen, z. B. Zeichenfolgen, Zahlen, null usw. Darüber hinaus gibt es Knoten zur Darstellung von Ausdrücken, die zur Steuerung des Programmausführungsflusses verwendet werden ( if konstruiert), und Knoten für Schleifen ( for , while ). Es gibt auch einen speziellen Knotentyp zur Darstellung von Klassen. Es ist ein Nachkomme der Node Basisklasse. Er erweitert diese Klasse, indem er Felder zum Speichern von Verweisen auf die Basisklasse und den Hauptteil der Klasse als separaten Knoten hinzufügt.
Konvertieren Sie das folgende Codefragment in einen abstrakten Syntaxbaum:

 class Component { constructor(content) {   this.content = content; } render() {   console.log(this.content) } } 

So wird seine schematische Darstellung aussehen.


Abstrakter Syntaxbaum

Nach dem Erstellen eines Baums wird jeder seiner Knoten in den entsprechenden ES5-Knoten umgewandelt. Anschließend wird dieser neue Baum in Code konvertiert, der dem ECMAScript 5-Standard entspricht. Suchen Sie während des Konvertierungsprozesses zunächst den Knoten, der am weitesten vom Stammknoten entfernt ist, und konvertieren Sie diesen Knoten anschließend in Code Verwenden von für jeden Knoten generierten Snippets. Danach wird der Vorgang wiederholt. Diese Technik wird als Tiefensuche bezeichnet .

Im obigen Beispiel wird zuerst der Code für die beiden MethodDefinition Knoten generiert, danach der Code für den ClassBody Knoten und schließlich der Code für den ClassDeclaration Knoten.

TypeScript-Transpilation


Ein weiteres beliebtes System, das Transpilation verwendet, ist TypeScript. Dies ist eine Programmiersprache, deren Code in ECMAScript 5-Code umgewandelt wird, der für jede JS-Engine verständlich ist. Es bietet eine neue Syntax zum Schreiben von JS-Anwendungen. So implementieren Sie die Component Klasse in TypeScript:

 class Component {   content: string;   constructor(content: string) {       this.content = content;   }   render() {       console.log(this.content)   } } 

Hier ist der abstrakte Syntaxbaum für diesen Code.


Abstrakter Syntaxbaum

TypeScript unterstützt die Vererbung.

 class InputField extends Component {   constructor(value: string) {       const content = `<input type="text" value="${value}" />`;       super(content);   } } 

Hier ist das Ergebnis der Transpilation dieses Codes:

 var InputField = /** @class */ (function (_super) {   __extends(InputField, _super);   function InputField(value) {       var _this = this;       var content = "<input type=\"text\" value=\"" + value + "\" />";       _this = _super.call(this, content) || this;       return _this;   }   return InputField; }(Component)); 

Wie Sie sehen, handelt es sich wieder um einen ES5-Code, in dem zusätzlich zu Standardkonstruktionen einige Funktionen aus der TypeScript-Bibliothek aufgerufen werden. Die Funktionen der Funktion __extends() ähneln denen, über die wir am Anfang dieses Materials gesprochen haben.

Dank der weit verbreiteten Einführung von Babel und TypeScript sind Mechanismen zum Deklarieren von Klassen und zum Organisieren der klassenbasierten Vererbung zu Standardwerkzeugen für die Strukturierung von JS-Anwendungen geworden. Dies trug dazu bei, diese Mechanismen in Browsern zu unterstützen.

Unterstützung für Browserklassen


Die Klassenunterstützung wurde 2014 im Chrome-Browser angezeigt. Auf diese Weise kann der Browser mit Klassendeklarationen ohne Verwendung von Transpilation oder Hilfsbibliotheken arbeiten.


Arbeiten mit Klassen in der Chrome JS-Konsole

Tatsächlich ist die Browserunterstützung für diese Mechanismen nichts anderes als syntaktischer Zucker. Diese Konstrukte werden in dieselben Grundstrukturen konvertiert, die bereits von der Sprache unterstützt werden. Selbst wenn Sie die neue Syntax auf einer niedrigeren Ebene verwenden, sieht alles so aus, als würden Konstruktoren erstellt und Prototypen von Objekten bearbeitet:


Klassenunterstützung ist syntaktischer Zucker

Klassenunterstützung in V8


Lassen Sie uns darüber sprechen, wie die Unterstützung der ES6-Klasse in der V8-JS-Engine funktioniert. Im vorherigen Material zu abstrakten Syntaxbäumen haben wir darüber gesprochen, dass das System JS-Code bei der Vorbereitung für die Ausführung analysiert und auf seiner Grundlage einen abstrakten Syntaxbaum bildet. Beim Parsen von Konstruktionen von Klassendeklarationen fallen Knoten vom Typ ClassLiteral in den abstrakten Syntaxbaum.

Diese Knoten speichern einige interessante Dinge. Erstens ist es ein Konstruktor als separate Funktion, und zweitens ist es eine Liste von Klasseneigenschaften. Dies können Methoden, Getter, Setter, öffentliche oder private Felder sein. Ein solcher Knoten speichert außerdem einen Verweis auf die übergeordnete Klasse, der die Klasse erweitert, für die der Knoten gebildet wird, und der wiederum den Konstruktor, die Liste der Eigenschaften und eine Verknüpfung zu seiner eigenen übergeordneten Klasse speichert.

Nachdem der neue ClassLiteral Knoten in Code umgewandelt wurde , wird er in Konstrukte konvertiert, die aus Funktionen und Prototypen bestehen.

Zusammenfassung


Der Autor dieses Materials sagt, dass SessionStack bestrebt ist, den Code seiner Bibliothek so vollständig wie möglich zu optimieren, da es schwierige Aufgaben beim Sammeln von Informationen über alles, was auf Webseiten passiert, lösen muss. Bei der Lösung dieser Probleme sollte die Bibliothek die zu analysierende Seite nicht verlangsamen. Für die Optimierung dieser Ebene müssen die kleinsten Details des JavaScript-Ökosystems berücksichtigt werden, die sich auf die Leistung auswirken, insbesondere die Merkmale der Anordnung von Klassen und Vererbungsmechanismen in ES6.

Liebe Leser! Verwenden Sie ES6-Syntaxkonstrukte für die Arbeit mit Klassen in JavaScript?

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


All Articles