Was ist das und was frisst es?


Foto von Sebastian Herrmann .

Guten Tag, Freunde!

Ich präsentiere Ihnen die Übersetzung von Daniel James ' Artikel „Was ist das? Warum ist das so? "

Was ist das und was frisst es?


Als ich anfing, JavaScript zu lernen, schien mir das Konzept äußerst verwirrend.

Einleitung


Das rasche Anwachsen der Popularität von JS ist teilweise auf eine niedrige Eintrittsschwelle zurückzuführen. Features wie Funktionen und diese funktionieren normalerweise wie erwartet. Um ein Profi in JS zu werden, müssen Sie nicht viele kleine Details und Details kennen (ich würde damit argumentieren - ca. Per.). Aber einmal stößt jeder Entwickler auf einen Fehler, der durch diesen Wert verursacht wird.

Danach möchten Sie verstehen, wie dies in JS funktioniert. Ist dies ein Konzept für die objektorientierte Programmierung (OOP)? Ist JS eine objektorientierte Programmiersprache (OOJP)? Wenn Sie dies "googeln", erhalten Sie als Antwort die Erwähnung einiger Prototypen. Was für Prototypen? Wofür wurde das "neue" Schlüsselwort verwendet, bevor Klassen in JS auftauchten?

All diese Dinge sind eng miteinander verbunden. Aber bevor ich erkläre, wie das funktioniert, werde ich mir einen kleinen Exkurs erlauben. Ich möchte ein wenig darüber sprechen, warum JS das ist, was es ist.

OOP in JS


Das Paradigma der Prototypprogrammierung (Vererbung) in JS ist eines der Kennzeichen von OOP. Schon vor dem Aufkommen der JS-Klassen gab es OOJP. JS ist eine einfache Sprache, die nur wenige Dinge aus OOP verwendet. Die wichtigsten davon sind Funktionen, Abschlüsse, Prototypen, Objektliterale und das neue Schlüsselwort.

Verkapselung und Wiederverwendbarkeit mit Verschlüssen


Lassen Sie uns die Counter-Klasse erstellen. Diese Klasse sollte Methoden zum Zurücksetzen und Inkrementieren des Zählers enthalten. Wir können so etwas schreiben:

function Counter(initialValue = 0){ let _count = initialValue return { reset: function(){ _count = 0 }, next: function(){ return ++_count } } } const myCounter = Counter() console.log(myCounter.next()) // 1 

In diesem Fall haben wir uns darauf beschränkt, Funktionen und Objektliterale ohne diese oder neue zu verwenden. Ja, wir haben schon etwas von OOP bekommen. Wir haben die Möglichkeit, neue Instanzen von Counter zu erstellen. Jede Instanz von Counter verfügt über eine eigene interne Variablenanzahl. Wir haben die Kapselung und Wiederverwendung rein funktional implementiert.

Leistungsproblem


Angenommen, wir schreiben ein Programm, das eine große Anzahl von Zählern verwendet. Jeder Zähler hat seine eigenen Methoden zurückgesetzt und nächste (Zähler (). Zurücksetzen! = Zähler (). Zurücksetzen). Das Erstellen solcher Abschlüsse für jede Methode jeder Instanz erfordert eine enorme Menge an Speicher! Eine solche Architektur ist "nicht nachhaltig". Daher müssen wir eine Möglichkeit finden, in jeder Instanz von Counter nur Verweise auf die von ihr verwendeten Methoden zu speichern (genau das tun alle OOJPs wie Java).

Wir könnten dieses Problem wie folgt lösen (ohne zusätzliche Sprachfunktionen einzubeziehen):

 let Counter = { reset: function(counter){ counter._count = 0 }, next: function(counter){ return ++counter._count }, new: function(initialValue = 0){ return { _count: initialValue } } } const myCounter = Counter.new() console.log(Counter.next(myCounter)) // 1 

Dieser Ansatz löst das Leistungsproblem, aber wir mussten einen ernsthaften Kompromiss eingehen, der darin besteht, dass ein hohes Maß an Programmiererbeteiligung an der Programmausführung erforderlich ist (um sicherzustellen, dass der Code funktioniert). Ohne zusätzliche Tools müssten wir uns mit diesem Ansatz zufrieden geben.

Dies eilt zur Rettung


Wir schreiben unser Beispiel folgendermaßen um:

 let Counter = { reset: function(){ this._count = 0 }, next: function(){ return ++this._count }, new: function(initialValue = 0){ return { _count: initialValue, //          reset: Counter.reset, next: Counter.next } } } const myCounter = Counter.new() myCounter.next() // ,  reset     myCounter (Counter.new()).reset() console.log(myCounter.next()) // 2 

Beachten Sie, dass wir weiterhin einfache Reset- und Next-Funktionen erstellen (Counter.new (). Reset == Counter.new (). Reset). Im vorherigen Beispiel mussten wir einen Instanzdeskriptor für gemeinsam implementierte Methoden bereitstellen, damit das Programm funktioniert. Jetzt rufen wir einfach myCounter.next () auf und verweisen damit auf die Instanz. Aber wie geht das? Reset und next werden im Counter-Objekt deklariert. Woher weiß JS, worauf sich das bezieht, wenn es eine Funktion aufruft?

Funktionsaufruf in JS


Sie wissen sehr gut, dass Funktionen in JS eine Aufrufmethode haben (es gibt auch eine Methode apply; der Unterschied zwischen diesen Methoden ist nicht signifikant. Der Unterschied besteht darin, wie wir die Parameter übergeben: in apply als Array, in call durch Kommas getrennt - ca. Per.) . Mit call legen Sie fest, was dies beim Aufruf der Funktion bedeutet:

 const myCounter = Counter.new() Counter.next.call(myCounter) 

Dies ist eigentlich das, was die Punktnotation hinter den Kulissen macht, wenn wir die Funktion aufrufen. lhs.fn () ist identisch mit fn.call (lhs).

Dies ist also eine spezielle Kennung, die beim Aufruf der Funktion gesetzt wird.

Probleme beginnen


Angenommen, Sie möchten einen Zähler erstellen und seinen Wert jede Sekunde erhöhen. So geht's:

 const myCounter = Counter.new() setInterval(myCounter.next, 1000) //    console.log(`Why is ${myCounter.next()} still 0?`) //  myCounter.next()    0? 

Sehen Sie hier einen Fehler? Wenn setInterval gestartet wird, ist der Wert nicht definiert, sodass nichts passiert. Dieses Problem kann wie folgt gelöst werden:

 const myCounter = Counter.new() setInterval(function(){ myCounter.next() }, 1000) 

Ein bisschen über binden


Es gibt einen anderen Weg, um dieses Problem zu lösen:

 function bindThis(fn, _this){ return function(...args){ return fn.call(_this, ...args) } } const myCounter = Counter.new() setInterval(bindThis(myCounter.next, myCounter), 1000) 

Mit der Funktion bindThis factory können wir sicher sein, dass Counter.next myCounter immer so aufruft, unabhängig davon, wie die neue Funktion aufgerufen wird. Tatsächlich ändern wir die Funktion von Counter.next nicht. JS verfügt über eine integrierte Bindemethode. Daher können wir das obige Beispiel folgendermaßen umschreiben: setInterval (myCounter.next.bind (myCounter), 1000).

Wir arbeiten mit Prototypen


Im Moment haben wir eine nette Gegenklasse, aber es ist immer noch ein bisschen "schief". Dies sind die folgenden Zeilen:

 // ... reset: Counter.reset, next: Counter.next, // ... 

Wir brauchen eine bessere Möglichkeit, Klassenmethoden mit ihren Instanzen zu teilen. Prototypen leisten dabei hervorragende Arbeit. Wenn Sie auf die Eigenschaft einer Funktion oder eines Objekts verweisen, die bzw. das nicht vorhanden ist, sucht JS im Prototyp dieser Funktion oder dieses Objekts nach dieser Eigenschaft (dann im Prototypprototyp usw. auf Object.prototype oben in der Prototypkette - ca. Trans.). Sie können den Prototyp eines Objekts mit Object.setPrototypeOf definieren. Lassen Sie uns unsere Counter-Klasse mit Prototypen umschreiben:

 let Counter = { reset: function(){ this._count = 0 }, next: function(){ return ++this._count }, new: function(initialValue = 0){ this._count = initialValue } } function newInstanceOf(klass, ...args){ //       "klass",  "class"   const instance = {} Object.setPrototypeOf(instance, klass) instance.new(...args) return instance } const myCounter = newInstanceOf(Counter) console.log(myCounter.next()) // 1 

Stichwort "neu"


Die Verwendung von setPrototypeOf ist der Funktionsweise des "neuen" Operators sehr ähnlich. Der Unterschied besteht darin, dass new den Prototyp-Konstruktor der übergebenen Funktion verwendet. Anstatt ein Objekt für unsere Methoden zu erstellen, übergeben wir diese daher an den Prototyp des Funktionskonstruktors:

 function Counter(initialValue = 0){ this._count = initialValue } Counter.prototype.reset = function(){ this._count = 0 } Counter.prototype.next = function(){ return ++this._count } const myCounter = new Counter() console.log(`${myCounter.next()}`) // 1 

Schließlich haben wir den Code in der Form, in der er in der Praxis zu finden ist. Bevor Klassen in JS erschienen, war dies der Standardansatz zum Erstellen und Initialisieren von Klassen.

Schlüsselwort "class"


Ich hoffe, dass Sie jetzt verstehen, warum wir den Prototyp des Funktionskonstruktors verwenden und wie dies in Funktionsmethoden funktioniert. Unser Code kann jedoch verbessert werden. Glücklicherweise gibt es heute in JS eine bessere Möglichkeit, Klassen zu deklarieren:

 class Counter { reset(){ this._count = 0 } next(){ return ++this._count } constructor(initialValue = 0){ this._count = initialValue } } const myCounter = new Counter() console.log(`${myCounter.next()}`) // 1 

Das Schlüsselwort "Klasse" macht insbesondere unter "Katze" nichts. Sie können es sich als syntaktischen Zucker vorstellen, als Hülle für den „Prototyp“ -Ansatz. Wenn Sie den auf ES3 ausgerichteten Transporter ausführen, erhalten Sie Folgendes:

 var Counter = /** @class **/ (function(){ function Counter(initialValue){ if(initialValue === void 0) { initialValue = 0 } this._count = initialValue } Counter.prototype.reset = function(){ this._count = 0 } Counter.prototype.next = function(){ ++this._count } return Counter }()); var myCounter = new Counter() console.log(myCounter.next()) 

Beachten Sie, dass der Transpiler Code generiert hat, der fast identisch mit dem vorherigen Beispiel ist.

Pfeilfunktionen


Wenn Sie in den letzten 5 Jahren Code in JS geschrieben haben, werden Sie überrascht sein, dass ich Pfeilfunktionen erwähne. Mein Rat: Verwenden Sie immer Pfeilfunktionen, bis Sie wirklich eine reguläre Funktion benötigen. Es ist passiert, dass die Definition des Konstruktors und der Methoden der Klasse genau der Fall ist, wenn wir gewöhnliche Funktionen verwenden müssen. Eine der Eigenschaften von Pfeilfunktionen ist die Verschleierung.

Dies funktioniert in Pfeilfunktionen


Einige gehen möglicherweise davon aus, dass Pfeilfunktionen beim Erstellen den aktuellen Wert von diesem annehmen. Dies ist aus technischer Sicht falsch (die Bedeutung ist nicht definiert, sie stammt aus der lexikalischen Umgebung), aber dies ist ein gutes mentales Modell. Eine Pfeilfunktion wie diese:

 const myArrowFunction = () => { this.doSomething() } 

Sie können es folgendermaßen umschreiben:
 const _this = this const myRegularFunction = function(){ _this.doSomething() } 

Danke für die Aufmerksamkeit. Alles Gute.

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


All Articles