Stoppen Sie die Verschachtelungsfunktionen! (Aber nicht alle von ihnen)

JavaScript ist über fünfzehn Jahre alt. Trotzdem wird die Sprache immer noch von der Mehrheit der Entwickler und Designer missverstanden, die die Sprache verwenden. JavaScript ist einer der mächtigsten, aber missverstandenen Aspekte von Funktionen. JavaScript ist zwar äußerst wichtig, kann aber durch den Missbrauch Ineffizienz verursachen und die Leistung einer Anwendung beeinträchtigen.


Bevorzugen Sie ein Video-Tutorial?


Leistung ist wichtig

In den Kinderschuhen des Web war die Leistung nicht sehr wichtig.

In den Kinderschuhen des Web war die Leistung nicht sehr wichtig. Von den 56K (oder schlechteren) DFÜ-Verbindungen zu einem 133MHz-Pentium-Computer eines Endbenutzers mit 8 MB RAM wurde erwartet, dass das Web langsam ist (obwohl dies nicht jeden davon abgehalten hat, sich darüber zu beschweren). Aus diesem Grund wurde JavaScript entwickelt, um die einfache Verarbeitung (z. B. Formularvalidierung) an den Browser zu übertragen, wodurch bestimmte Aufgaben für den Endbenutzer einfacher und schneller werden. Anstatt ein Formular auszufüllen, auf "Senden" zu klicken und mindestens dreißig Sekunden darauf zu warten, dass Sie falsche Daten in ein Feld eingegeben haben, können Web-Autoren mit JavaScript Ihre Eingabe überprüfen und Sie auf Fehler hinweisen, bevor das Formular übermittelt wird.

Schneller Vorlauf bis heute. Endbenutzer genießen Multi-Core- und Multi-GHz-Computer, viel RAM und schnelle Verbindungsgeschwindigkeiten. JavaScript ist nicht länger auf die Überprüfung von Formulare beschränkt, es kann jedoch große Datenmengen verarbeiten, jeden beliebigen Teil einer Seite im laufenden Betrieb ändern, Daten vom Server senden und empfangen und einer sonst statischen Seite im Namen Interaktivität verleihen Verbesserung der Benutzererfahrung. Es ist ein Muster, das in der gesamten Computerbranche weithin bekannt ist: Dank wachsender Systemressourcen können Entwickler komplexere und ressourcenabhängige Betriebssysteme und Software schreiben. Aber selbst bei dieser großen und ständig wachsenden Menge an Ressourcen müssen Entwickler auf die Menge an Ressourcen achten, die ihre App benötigt, insbesondere im Web.

Die heutigen JavaScript-Engines sind den Engines von vor zehn Jahren um Lichtjahre voraus, optimieren jedoch nicht alles. Was sie nicht optimieren, bleibt den Entwicklern überlassen.

Es gibt auch eine ganze Reihe von webfähigen Geräten, Smartphones und Tablets, die mit einer begrenzten Anzahl von Ressourcen ausgeführt werden. Ihre heruntergefahrenen Betriebssysteme und Apps sind sicherlich ein Hit, aber die großen Hersteller von mobilen Betriebssystemen (und sogar Anbieter von Desktop-Betriebssystemen) suchen nach Web-Technologien als ihrer bevorzugten Entwicklerplattform und drängen JavaScript-Entwickler, um sicherzustellen, dass ihr Code effizient und performant ist.

Eine Anwendung mit schlechter Leistung führt zu einer Verschwendung.

Am wichtigsten ist, dass die Benutzererfahrung von einer guten Leistung abhängt. Hübsche und natürliche Benutzeroberflächen tragen sicherlich zur Benutzererfahrung bei, aber eine Anwendung mit schlechter Leistung führt zu einer schlechten Benutzererfahrung. Wenn Benutzer Ihre Software nicht verwenden möchten, worauf kommt es dann an, sie zu schreiben? Daher ist es absolut wichtig, dass JavaScript-Entwickler in der heutigen Zeit der weborientierten Entwicklung den bestmöglichen Code schreiben.

Was hat das alles mit Funktionen zu tun??

Wo Sie Ihre Funktionen definieren, wirkt sich auf die Leistung Ihrer Anwendung aus.

Es gibt viele JavaScript-Anti-Patterns, aber eines, das Funktionen enthält, ist etwas populär geworden - vor allem in der Masse, die JavaScript dazu zwingt, Features in anderen Sprachen zu emulieren (Features wie Privacy). Es ist das Verschachteln von Funktionen in anderen Funktionen, und wenn es falsch ausgeführt wird, kann dies negative Auswirkungen auf Ihre Anwendung haben.

Es ist wichtig zu wissen, dass dieses Anti-Pattern nicht für alle Instanzen verschachtelter Funktionen gilt, sondern normalerweise durch zwei Merkmale definiert wird. Erstens ist die Erstellung der fraglichen Funktion normalerweise zurückgestellt, was bedeutet, dass die verschachtelte Funktion nicht zum Zeitpunkt des Ladens von der JavaScript-Engine erstellt wird. Das an und für sich ist keine schlechte Sache, aber es ist das zweite Merkmal, das die Leistung behindert: Die verschachtelte Funktion wird aufgrund wiederholter Aufrufe der äußeren Funktion wiederholt erstellt. Es ist zwar leicht zu sagen, "alle verschachtelten Funktionen sind schlecht", das ist jedoch sicherlich nicht der Fall, und Sie können problematische verschachtelte Funktionen erkennen und beheben, um Ihre Anwendung zu beschleunigen.


Verschachtelungsfunktionen in normalen Funktionen

Das erste Beispiel für dieses Antimuster ist das Verschachteln einer Funktion innerhalb einer normalen Funktion. Hier ist ein stark vereinfachtes Beispiel:

function foo (a, b) Funktionsleiste () return a + b;  return bar ();  foo (1, 2);

Sie können diesen exakten Code nicht schreiben, aber es ist wichtig, das Muster zu erkennen. Eine äußere Funktion, foo (), enthält eine innere Funktion, Bar(), und nennt diese innere Funktion, um Arbeit zu erledigen. Viele Entwickler vergessen, dass Funktionen Werte in JavaScript sind. Wenn Sie eine Funktion in Ihrem Code deklarieren, erstellt die JavaScript-Engine ein entsprechendes Funktionsobjekt - einen Wert, der einer Variablen zugewiesen oder an eine andere Funktion übergeben werden kann. Das Erstellen eines Funktionsobjekts ähnelt dem eines anderen Werttyps. Die JavaScript-Engine erstellt sie erst, wenn dies erforderlich ist. Im Fall des obigen Codes erstellt die JavaScript-Engine also nicht das Innere Bar() Funktion bis foo () führt aus. Wann foo () Ausgänge, die Bar() Funktionsobjekt wird zerstört.

Die Tatsache, dass foo () hat einen Namen, der besagt, dass er in der gesamten Anwendung mehrmals aufgerufen wird. Während einer Hinrichtung von foo () wäre in Ordnung, nachfolgende Aufrufe verursachen unnötige Arbeit für die JavaScript-Engine, da sie eine neu erstellen muss Bar() Funktionsobjekt für jeden foo () Ausführung. Also, wenn du anrufst foo () 100 Mal in einer Anwendung muss die JavaScript-Engine 100 erstellen und zerstören Bar() Funktionsobjekte. Große Sache, richtig? Die Engine muss bei jedem Aufruf andere lokale Variablen in einer Funktion erstellen. Warum sollten Sie sich also um Funktionen kümmern??

Im Gegensatz zu anderen Wertetypen ändern sich Funktionen normalerweise nicht. Eine Funktion wird erstellt, um eine bestimmte Aufgabe auszuführen. Daher ist es nicht sinnvoll, CPU-Zyklen zu verschwenden, die immer wieder einen statischen Wert erzeugen.

Idealerweise die Bar() Funktionsobjekte in diesem Beispiel sollten nur einmal erstellt werden, und das ist leicht zu erreichen - obwohl komplexere Funktionen natürlich umfangreiches Refactoring erfordern. Die Idee ist, das zu bewegen Bar() Deklaration außerhalb von foo () so dass das Funktionsobjekt nur einmal erstellt wird, wie folgt:

Funktion foo (a, b) Rückkehrstab (a, b);  Funktionsleiste (a, b) return a + b;  foo (1, 2);

Beachten Sie, dass das neue Bar() Funktion ist nicht genau so, wie es in war foo (). Weil das alte Bar() Funktion verwendet die ein und b Parameter in foo (), Die neue Version benötigte ein Refactoring, um diese Argumente für ihre Arbeit zu akzeptieren.

Je nach Browser ist dieser optimierte Code um 10% bis 99% schneller als die geschachtelte Version. Sie können den Test unter jsperf.com/nested-named-functions anzeigen und ausführen. Beachten Sie die Einfachheit dieses Beispiels. Ein Leistungszuwachs von 10% (am niedrigsten Ende des Leistungsspektrums) scheint nicht viel zu sein, wäre aber umso höher, je mehr verschachtelte und komplexe Funktionen betroffen sind.

Um das Problem möglicherweise zu verwirren, packen Sie diesen Code in eine anonyme, sich selbst ausführende Funktion wie die folgende:

(function () Funktion foo (a, b) Rückkehrleiste (a, b); Funktionsleiste (a, b) Rückkehr a + b; foo (1, 2); ());

Das Umschließen von Code in eine anonyme Funktion ist ein allgemeines Muster. Auf den ersten Blick scheint es, als würde dieser Code das zuvor erwähnte Leistungsproblem replizieren, indem der optimierte Code in eine anonyme Funktion eingebunden wird. Obwohl die Ausführung der anonymen Funktion einen geringfügigen Leistungsabfall zur Folge hat, ist dieser Code durchaus akzeptabel. Die selbstausführende Funktion dient nur dazu, das System einzuschließen und zu schützen foo () und Bar() Funktionen, aber noch wichtiger ist, dass die anonyme Funktion nur einmal ausgeführt wird - also die innere foo () und Bar() Funktionen werden nur einmal erstellt. Es gibt jedoch Fälle, in denen anonyme Funktionen genauso wie benannte Funktionen problematisch sind.


Anonyme Funktionen

In Bezug auf dieses Thema der Leistung können anonyme Funktionen gefährlicher sein als benannte Funktionen.

Es ist nicht die Anonymität der Funktion, die gefährlich ist, sondern wie Entwickler sie verwenden. Bei der Einrichtung von Ereignishandlern, Rückruffunktionen oder Iteratorfunktionen werden anonyme Funktionen verwendet. Der folgende Code weist beispielsweise a zu klicken Ereignislistener im Dokument:

document.addEventListener ("click", function (evt) alert ("Sie haben auf die Seite geklickt."););

Hier wird eine anonyme Funktion an den übergeben addEventListener () Methode zur Verdrahtung der klicken Ereignis auf dem Dokument; Die Funktion wird also jedes Mal ausgeführt, wenn der Benutzer irgendwo auf die Seite klickt. Um eine weitere häufige Verwendung anonymer Funktionen zu demonstrieren, betrachten Sie dieses Beispiel, in dem die jQuery-Bibliothek zur Auswahl aller verwendet wird Elemente im Dokument und iterieren mit dem jeder() Methode:

$ ("a"). each (Funktion (Index) this.style.color = "red";);

In diesem Code wurde die anonyme Funktion an das jQuery-Objekt übergeben jeder() Methode wird für jeden ausgeführt Element im Dokument gefunden. Im Gegensatz zu benannten Funktionen, bei denen ein wiederholter Aufruf impliziert wird, ist die wiederholte Ausführung einer großen Anzahl anonymer Funktionen eher explizit. Aus Performancegründen ist es unabdingbar, dass sie effizient und optimiert sind. Schauen Sie sich das folgende (nochmal stark vereinfachte) jQuery-Plugin an:

$ .fn.myPlugin = Funktion (Optionen) return this.each (Funktion () var $ this = $ (this); Funktion changeColor () $ this.css (color: options.color); changeColor ();); ;

Dieser Code definiert ein extrem einfaches Plugin, das aufgerufen wird myPlugin; Es ist so einfach, dass viele gängige Plugin-Merkmale fehlen. Normalerweise werden Plugin-Definitionen in selbstausführende anonyme Funktionen eingebettet. Normalerweise werden Standardwerte für Optionen angegeben, um sicherzustellen, dass gültige Daten verfügbar sind. Diese Dinge wurden aus Gründen der Klarheit entfernt.

Der Zweck dieses Plugins besteht darin, die Farbe der ausgewählten Elemente in das zu ändern, was im angegeben ist Optionen Objekt übergeben an myPlugin () Methode. Dies geschieht durch Übergabe einer anonymen Funktion an jeder() iterator, wodurch diese Funktion für jedes Element im jQuery-Objekt ausgeführt wird. Innerhalb der anonymen Funktion wird eine innere Funktion aufgerufen Farbe ändern() führt die eigentliche Arbeit aus, um die Farbe des Elements zu ändern. Wie geschrieben, ist dieser Code ineffizient, weil Sie es erraten haben Farbe ändern() Funktion wird innerhalb der Iterationsfunktion definiert? die JavaScript-Engine neu erstellen Farbe ändern() bei jeder Wiederholung.

Diesen Code effizienter zu machen, ist ziemlich einfach und folgt dem gleichen Muster wie zuvor: Refactor the Farbe ändern() Funktion, die außerhalb der enthaltenen Funktionen definiert wird, und die Informationen erhalten, die sie für ihre Arbeit benötigen. In diesem Fall, Farbe ändern() benötigt das jQuery-Objekt und den neuen Farbwert. Der verbesserte Code sieht folgendermaßen aus:

function changeColor ($ obj, color) $ obj.css (color: color);  $ .fn.myPlugin = Funktion (Optionen) return this.each (Funktion () var $ this = $ (this); changeColor ($ this, options.color);); ;

Interessanterweise erhöht dieser optimierte Code die Leistung um einen viel geringeren Spielraum als der foo () und Bar() Beispiel: Chrome führt das Paket mit einem Leistungszuwachs von 15% an (jsperf.com/function-nesting-with-jquery-plugin). Die Wahrheit ist, dass der Zugriff auf das DOM und die Verwendung der API von jQuery der Performance, insbesondere jQuery, einen eigenen Hit verleiht jeder(), Dies ist im Vergleich zu den nativen Schleifen von JavaScript notorisch langsam. Denken Sie aber wie zuvor an die Einfachheit dieses Beispiels. Je mehr verschachtelte Funktionen, desto größer ist der Leistungsgewinn bei der Optimierung.

Verschachtelungsfunktionen in Konstruktorfunktionen

Eine weitere Variante dieses Antimusters ist das Verschachteln von Funktionen in Konstruktoren, wie unten gezeigt:

Funktion Person (Vorname, Nachname) this.FirstName = Vorname; this.lastName = Nachname; this.getFullName = function () gibt this.firstName + "" + this.lastName zurück; ;  var jeremy = neue Person ("Jeremy", "McPeak"), Jeffrey = neue Person ("Jeffrey", "Way");

Dieser Code definiert eine aufgerufene Konstruktorfunktion Person(), und es repräsentiert (wenn es nicht offensichtlich ist) eine Person. Es akzeptiert Argumente, die den Vor- und Nachnamen einer Person enthalten, und speichert diese Werte in Vorname und Nachname Eigenschaften. Der Konstruktor erstellt auch eine aufgerufene Methode getFullName (); es verkettet das Vorname und Nachname Eigenschaften und gibt den resultierenden String-Wert zurück.

Wenn Sie ein Objekt in JavaScript erstellen, wird das Objekt im Arbeitsspeicher gespeichert

Dieses Muster ist in der heutigen JavaScript-Community durchaus üblich, da es die Privatsphäre emulieren kann, eine Funktion, für die JavaScript derzeit nicht konzipiert ist (beachten Sie, dass die Privatsphäre nicht im obigen Beispiel ist; Sie werden das später betrachten). Bei der Verwendung dieses Musters erzeugen Entwickler jedoch nicht nur Ineffizienz, sondern auch die Speicherauslastung. Wenn Sie ein Objekt in JavaScript erstellen, wird das Objekt im Arbeitsspeicher gespeichert. Es bleibt im Speicher, bis alle Verweise darauf auf gesetzt sind Null oder sind außerhalb des Geltungsbereichs. Im Falle der jeremy Objekt im obigen Code die zugewiesene Funktion getFullName wird normalerweise so lange im Speicher gespeichert, wie die jeremy Objekt befindet sich im Speicher. Wenn der Jeffrey Objekt angelegt, ein neues Funktionsobjekt angelegt und zugewiesen Jeffrey's getFullName Mitglied, und es verbraucht auch so lange Speicher Jeffrey ist in Erinnerung. Das Problem hier ist das jeremy.getFullName ist ein anderes Funktionsobjekt als jeffrey.getFullName (jeremy.getFullName === jeffrey.getFullName führt in falsch; Führen Sie diesen Code unter http://jsfiddle.net/k9uRN/) aus. Sie haben beide dasselbe Verhalten, sind aber zwei völlig unterschiedliche Funktionsobjekte (und verbrauchen somit jeweils Speicher). Sehen Sie sich zur Verdeutlichung Abbildung 1 an:

Abbildung 1

Hier sehen Sie die jeremy und Jeffrey Objekte, von denen jedes seine eigenen hat getFullName () Methode. Also jeder Person Das erstellte Objekt hat sein eigenes Unikat getFullName () Methode, von der jede ihren eigenen Speicherplatz verbraucht. Stellen Sie sich vor, Sie schaffen 100 Person Objekte: wenn jeweils getFullName () Diese Methode verbraucht 4 KB Speicher, dann 100 Person Objekte würden mindestens 400 KB Speicher benötigen. Das kann sich addieren, aber durch die Verwendung von kann es drastisch reduziert werden Prototyp Objekt.

Verwenden Sie den Prototyp

Wie bereits erwähnt, handelt es sich bei Funktionen um Objekte in JavaScript. Alle Funktionsobjekte haben eine Prototyp Diese Eigenschaft ist nur für Konstruktorfunktionen nützlich. Kurz gesagt, die Prototyp Die Eigenschaft ist buchstäblich ein Prototyp zum Erstellen von Objekten. Was im Prototyp einer Konstruktorfunktion definiert ist, wird von allen Objekten gemeinsam genutzt, die von dieser Konstruktorfunktion erstellt werden.

Leider werden Prototypen in der JavaScript-Ausbildung nicht genug betont.

Leider werden Prototypen in der JavaScript-Ausbildung nicht genug betont, aber sie sind absolut notwendig für JavaScript, da es auf Prototypen basiert und darauf basiert - es ist eine Prototypensprache. Auch wenn Sie das Wort nie eingegeben haben Prototyp In Ihrem Code werden sie hinter den Kulissen verwendet. Zum Beispiel jede native String-basierte Methode, wie Teilt(), substr (), oder ersetzen(), sind definiert am String ()Prototyp. Prototypen sind für die JavaScript-Sprache so wichtig, dass Sie, wenn Sie sich nicht für die prototypische Natur von JavaScript interessieren, ineffizienten Code schreiben. Betrachten Sie die obige Implementierung des Person Datentyp: Erstellen eines Person object erfordert, dass die JavaScript-Engine mehr Arbeit verrichtet und mehr Speicherplatz zuweist.

Also, wie kann man das benutzen? Prototyp Eigenschaft macht diesen Code effizienter? Zuerst werfen Sie einen Blick auf den überarbeiteten Code:

Funktion Person (Vorname, Nachname) this.FirstName = Vorname; this.lastName = Nachname;  Person.prototype.getFullName = function () gibt diesen.ErstenName + "" + diesen.Lastennamen zurück; ; var jeremy = neue Person ("Jeremy", "McPeak"), Jeffrey = neue Person ("Jeffrey", "Way");

Hier die getFullName () Die Methodendefinition wird aus dem Konstruktor heraus auf den Prototyp verschoben. Diese einfache Änderung hat folgende Auswirkungen:

  • Der Konstruktor führt weniger Arbeit aus und ist daher schneller (18% bis 96% schneller). Führen Sie den Test in Ihrem Browser aus, wenn Sie möchten.
  • Das getFullName () Methode wird nur einmal erstellt und von allen geteilt Person Objekte (jeremy.getFullName === jeffrey.getFullName führt in wahr; Führen Sie diesen Code unter http://jsfiddle.net/Pfkua/) aus. Aus diesem Grund jeder Person Objekt benötigt weniger Speicher.

Siehe Abbildung 1 und beachten Sie, wie jedes Objekt seine eigenen hat getFullName () Methode. Nun das getFullName () auf dem Prototyp definiert ist, ändert sich das Objektdiagramm und ist in Abbildung 2 dargestellt:

Figur 2

Das jeremy und Jeffrey Objekte haben keine eigenen mehr getFullName () Methode, aber die JavaScript-Engine findet es auf Person()Prototyp. In älteren JavaScript-Engines kann das Auffinden einer Methode für den Prototyp einen Performance-Treffer nach sich ziehen, in den heutigen JavaScript-Engines jedoch nicht. Die Geschwindigkeit, mit der moderne Motoren Prototypmethoden finden, ist extrem schnell.

Privatsphäre

Aber wie sieht es mit der Privatsphäre aus? Immerhin wurde dieses Anti-Muster aus einem wahrgenommenen Bedürfnis nach privaten Objektmitgliedern geboren. Wenn Sie mit dem Muster nicht vertraut sind, werfen Sie einen Blick auf den folgenden Code:

Funktion Foo (paramOne) var thisIsPrivate = paramOne; this.bar = function () return thisIsPrivate; ;  var foo = new Foo ("Hallo, Datenschutz!"); alert (foo.bar ()); // Warnungen "Hallo, Datenschutz!"

Dieser Code definiert eine aufgerufene Konstruktorfunktion Foo (), und es hat einen Parameter aufgerufen paramOne. Der Wert, an den übergeben wurde Foo () wird in einer lokalen Variablen gespeichert das ist privat. Beachten Sie, dass das ist privat ist eine Variable, keine Eigenschaft; es ist also außerhalb von nicht erreichbar Foo (). Es gibt auch eine Methode, die im Konstruktor definiert ist und als Methode bezeichnet wird Bar(). weil Bar() ist innerhalb definiert Foo (), es hat Zugriff auf die das ist privat Variable. Wenn du also ein Foo Objekt und Anruf Bar(), der zugewiesene Wert das ist privat ist zurück gekommen.

Der zugewiesene Wert das ist privat bleibt erhalten. Es ist nicht außerhalb von erreichbar Foo (), und somit ist es vor äußeren Modifikationen geschützt. Das ist toll, richtig? Ja und nein. Es ist verständlich, warum einige Entwickler Datenschutz in JavaScript emulieren möchten: Sie können sicherstellen, dass die Daten eines Objekts vor Manipulationen von außen geschützt sind. Gleichzeitig führen Sie Ineffizienz in Ihren Code ein, indem Sie den Prototyp nicht verwenden.

Also nochmal, wie sieht es mit der Privatsphäre aus? Nun, das ist einfach: Tu es nicht. Die Sprache unterstützt derzeit keine privaten Objektmitglieder offiziell, obwohl sich dies bei einer zukünftigen Überarbeitung der Sprache ändern kann. Anstelle der Verwendung von Closures zum Erstellen privater Mitglieder besteht die Konvention zur Bezeichnung "privater Mitglieder" darin, dem Bezeichner einen Unterstrich voranzustellen (dh: _das ist privat). Der folgende Code schreibt das vorherige Beispiel mit der Konvention neu:

Funktion Foo (paramOne) this._thisIsPrivate = paramOne;  Foo.prototype.bar = function () return this._thisIsPrivate; ; var foo = new Foo ("Hallo, Konvention zum Schutz der Privatsphäre!"); alert (foo.bar ()); // Warnungen "Hallo, Konvention zum Schutz der Privatsphäre!"

Nein, es ist nicht privat, aber die Unterstrich-Konvention sagt im Grunde "Fass mich nicht an". Bis JavaScript private Eigenschaften und Methoden vollständig unterstützt, hätten Sie nicht lieber effizienteren und performanteren Code als Datenschutz? Die richtige Antwort lautet: Ja!


Zusammenfassung

Wenn Sie Funktionen in Ihrem Code definieren, wirkt sich dies auf die Leistung Ihrer Anwendung aus. Denken Sie daran, wenn Sie Ihren Code schreiben. Verschachteln Sie keine Funktionen in einer häufig aufgerufenen Funktion. Dies verschwendet CPU-Zyklen. Umfassen Sie als Konstruktorfunktion den Prototyp. Andernfalls führt dies zu ineffizientem Code. Entwickler schreiben schließlich Software, die vom Benutzer verwendet werden kann, und die Leistung einer Anwendung ist für die Benutzererfahrung ebenso wichtig wie die Benutzeroberfläche.