Die letzten Jahre haben uns gezeigt, dass sich die Webentwicklung verändert. Da viele Funktionen und APIs zu den Browsern hinzugefügt wurden, mussten wir sie auf die richtige Weise nutzen. Die Sprache, der wir diese Ehre verdanken, war JavaScript.
Anfänglich waren die Entwickler nicht von der Konzeption überzeugt und hatten überwiegend negative Eindrücke bei der Verwendung dieses Skripts. Mit der Zeit stellte sich heraus, dass diese Sprache ein großes Potenzial hat, und die nachfolgenden ECMAScript-Standards machen einige der Mechanismen menschlicher und einfach besser. In diesem Artikel werfen wir einen Blick auf einige von ihnen.
Arten von Werten in JS
Die bekannte Wahrheit über JavaScript ist, dass hier alles ein Objekt ist. Wirklich alles: Arrays, Funktionen, Strings, Zahlen und sogar Boolesche Werte. Alle Arten von Werten werden durch Objekte dargestellt und haben ihre eigenen Methoden und Felder. Wir können sie jedoch in zwei Kategorien einteilen: Primitive und strukturelle Werte. Die Werte der ersten Kategorie sind unveränderlich, d. h. wir können einer Variablen den neuen Wert zuweisen, aber den vorhandenen Wert selbst nicht ändern. Die zweite Kategorie stellt Werte dar, die verändert werden können, so dass sie als Sammlungen von Eigenschaften interpretiert werden sollten, die wir ersetzen oder einfach die dafür vorgesehenen Methoden aufrufen können.
Umfang der deklarierten Variablen
Bevor wir näher darauf eingehen, sollten wir erklären, was der Geltungsbereich bedeutet. Wir können sagen, dass der Geltungsbereich der einzige Bereich ist, in dem wir deklarierte Variablen verwenden können. Vor dem ES6-Standard konnten wir Variablen mit der var-Anweisung deklarieren und ihnen einen globalen oder lokalen Geltungsbereich geben. Der erste ist ein Bereich, der es uns erlaubt, auf einige Variablen an jeder Stelle der Anwendung zuzugreifen, der zweite ist nur einem bestimmten Bereich gewidmet - hauptsächlich einer Funktion.
Seit der Norm ES2015, JavaScript hat drei Möglichkeiten, Variablen zu deklarieren, die sich durch das Schlüsselwort unterscheiden. Die erste Möglichkeit wurde bereits beschrieben: Variablen, die mit dem Schlüsselwort var deklariert werden, sind auf den aktuellen Funktionskörper beschränkt. Der ES6-Standard erlaubt es uns, Variablen auf menschlichere Weise zu deklarieren - im Gegensatz zu var-Anweisungen werden Variablen, die mit const- und let-Anweisungen deklariert werden, nur auf den Block beschränkt. Allerdings behandelt JS die const-Anweisung recht ungewöhnlich, wenn man sie mit anderen Programmiersprachen - anstelle eines persistenten Wertes einen persistenten Verweis auf den Wert behält. Kurz gesagt, wir können die Eigenschaften eines mit einer const-Anweisung deklarierten Objekts ändern, aber wir können den Verweis auf diese Variable nicht überschreiben. Manche sagen, dass die var-Alternative in ES6 eigentlich eine let-Anweisung ist. Nein, das ist sie nicht, und die var-Anweisung wird wahrscheinlich auch nie zurückgezogen werden. Eine gute Praxis ist es, die Verwendung von var-Anweisungen zu vermeiden, da sie uns meist mehr Probleme bereiten. Im Gegenzug müssen wir const-Anweisungen missbrauchen, bis wir ihre Referenz ändern müssen - dann sollten wir let verwenden.
Beispiel für unerwartetes Verhalten des Bereichs
Beginnen wir mit folgendem Code:
(() => {
for (var i = 0; i {
console.log(`Wert von "i": ${i}`);
}, 1000);
}
})();
Es sieht so aus, als ob die for-Schleife den Wert i iteriert und nach einer Sekunde die Werte des Iterators protokolliert: 1, 2, 3, 4, 5. Das tut sie aber nicht. Wie wir bereits erwähnt haben, geht es bei der var-Anweisung darum, den Wert einer Variablen für den gesamten Funktionskörper aufrechtzuerhalten; das bedeutet, dass bei der zweiten, dritten und so weiter Iteration der Wert der Variablen i durch einen nächsten Wert ersetzt wird. Schließlich endet die Schleife und die Timeout-Ticks zeigen uns folgendes an: 5, 5, 5, 5, 5, 5. Der beste Weg, einen aktuellen Wert des Iterators zu erhalten, ist die Verwendung der let-Anweisung:
(() => {
for (let i = 0; i {
console.log(`Wert von "i": ${i}`);
}, 1000);
}
})();
Im obigen Beispiel behalten wir den Geltungsbereich des i-Wertes im aktuellen Iterationsblock bei. Das ist der einzige Bereich, in dem wir diese Variable verwenden können, und nichts kann sie von außerhalb dieses Bereichs überschreiben. Das Ergebnis ist in diesem Fall wie erwartet: 1 2 3 4 5. Schauen wir uns an, wie man diese Situation mit einer var-Anweisung behandeln kann:
(() => {
for (var i = 0; i {
setTimeout(() => {
console.log(`Wert von "j": ${j}`);
}, 1000);
})(i);
}
})();
Da es bei der var-Anweisung darum geht, den Wert innerhalb des Funktionsblocks zu halten, müssen wir eine definierte Funktion aufrufen, die ein Argument entgegennimmt - den Wert des aktuellen Zustands des Iterators - und dann einfach etwas tun. Nichts außerhalb der deklarierten Funktion wird den j-Wert überschreiben.
Beispiele für falsche Erwartungen an Objektwerte
Das am häufigsten begangene Verbrechen, das mir aufgefallen ist, betrifft das Ignorieren der Macht von Structurals und das Ändern ihrer Eigenschaften, die auch in anderen Teilen des Codes geändert werden. Werfen Sie einen kurzen Blick darauf:
const DEFAULT_VALUE = {
favoriteBand: 'The Weeknd'
};
const currentValue = DEFAULT_VALUE;
const bandInput = document.querySelector('#favorite-band');
const restoreDefaultButton = document.querySelector('#restore-button');
bandInput.addEventListener('input', () => {
currentValue.favoriteBand = bandInput.value;
}, false);
restoreDefaultButton.addEventListener('click', () => {
currentValue = DEFAULT_VALUE;
}, false);
Von Anfang an: Nehmen wir an, dass wir ein Modell mit Standardeigenschaften haben, das als Objekt gespeichert ist. Wir möchten eine Schaltfläche haben, die die Eingabewerte auf die Standardwerte zurücksetzt. Nachdem wir die Eingabe mit einigen Werten gefüllt haben, aktualisieren wir das Modell. Nach einem Moment denken wir, dass die Standardauswahl einfach besser war, also wollen wir sie wiederherstellen. Wir klicken auf die Schaltfläche... und nichts passiert. Und warum? Weil wir die Macht der referenzierten Werte ignorieren.
Dieser Teil: const currentValue = DEFAULTVALUE sagt dem JS folgendes: Nimm den Verweis auf den DEFAULTVALUE-Wert und weisen der Variablen currentValue diesen Wert zu. Der tatsächliche Wert wird nur einmal im Speicher abgelegt und beide Variablen zeigen auf ihn. Das Ändern einiger Eigenschaften an einer Stelle bedeutet, dass sie an einer anderen Stelle geändert werden. Wir haben einige Möglichkeiten, solche Situationen zu vermeiden. Eine, die unsere Anforderungen erfüllt, ist ein Spreizungsoperator. Ändern wir unseren Code:
const DEFAULT_VALUE = {
favoriteBand: 'The Weeknd'
};
const currentValue = { ...DEFAULT_VALUE };
const bandInput = document.querySelector('#favorite-band');
const restoreDefaultButton = document.querySelector('#restore-button');
bandInput.addEventListener('input', () => {
currentValue.favoriteBand = bandInput.value;
}, false);
restoreDefaultButton.addEventListener('click', () => {
currentValue = { ...DEFAULT_VALUE };
}, false);
In diesem Fall funktioniert der Spread-Operator folgendermaßen: Er nimmt alle Eigenschaften eines Objekts und erstellt ein neues Objekt, das mit diesen Eigenschaften gefüllt wird. Dadurch verweisen die Werte in currentValue und DEFAULT_VALUE nicht mehr auf dieselbe Stelle im Speicher, und alle Änderungen, die auf eine dieser Eigenschaften angewendet werden, wirken sich nicht auf die anderen aus.
Die Frage ist also: Geht es nur um die Verwendung des magischen Spread-Operators? In diesem Fall - ja, aber unsere Modelle können mehr Komplexität als dieses Beispiel erfordern. Wenn wir verschachtelte Objekte, Arrays oder andere Strukturen verwenden, wirkt sich der Spreizungsoperator des obersten referenzierten Wertes nur auf die oberste Ebene aus, und die referenzierten Eigenschaften teilen sich weiterhin denselben Platz im Speicher. Es gibt viele Lösungen, um dieses Problem zu behandeln, alles hängt von Ihren Bedürfnissen ab. Wir können Objekte in jeder Tiefenebene klonen oder, bei komplexeren Operationen, Werkzeuge wie immer verwenden, die es uns ermöglichen, unveränderlichen Code fast schmerzlos zu schreiben.
Mischen Sie alles zusammen
Ist eine Mischung aus Wissen über Geltungsbereiche und Wertetypen sinnvoll? Natürlich ist es das! Lassen Sie uns etwas bauen, das beides verwendet:
const useValue = (defaultValue) => {
const value = [...defaultValue];
const setValue = (newValue) => {
value.length = 0; // trickreiche Art, Array zu löschen
newValue.forEach((item, index) => {
wert[index] = item;
});
// einige andere Dinge tun
};
return [value, setValue];
};
const [animals, setAnimals] = useValue(['Katze', 'Hund']);
console.log(animals); // ['Katze', 'Hund']
setAnimals(['Pferd', 'Kuh']);
console.log(animals); // ['Pferd', 'Kuh']);
Lassen Sie uns erklären, wie dieser Code Zeile für Zeile funktioniert. Nun, die Funktion useValue erstellt ein Array auf der Grundlage des Arguments defaultValue; sie erstellt eine Variable und eine weitere Funktion, ihren Modifikator. Dieser Modifikator nimmt einen neuen Wert auf, der auf trickreiche Weise auf den vorhandenen Wert angewendet wird. Am Ende der Funktion geben wir den Wert und seinen Modifikator als Array-Werte zurück. Als nächstes verwenden wir die erstellte Funktion - deklarieren Tiere und setAnimals als zurückgegebene Werte. Verwenden Sie deren Modifikator, um zu prüfen, ob die Funktion die Tiervariable beeinflusst - ja, es funktioniert!
Aber Moment mal, was genau ist denn an diesem Code so ausgefallen? Die Referenz behält alle neuen Werte, und Sie können Ihre eigene Logik in diesen Modifikator einbauen, z. B. einige APIs oder ein Teil des Ökosystems die Ihren Datenfluss ohne Aufwand antreibt. Dieses trickreiche Muster wird häufig in moderneren JS-Bibliotheken verwendet, wo das funktionale Paradigma der Programmierung es uns ermöglicht, den Code weniger komplex und für andere Programmierer leichter lesbar zu halten.
Zusammenfassung
Wenn wir verstehen, wie die Sprachmechanik unter der Haube funktioniert, können wir bewussteren und leichteren Code schreiben. Auch wenn JS keine Low-Level-Sprache ist und uns zwingt, ein gewisses Wissen darüber zu haben, wie Speicher zugewiesen und gespeichert wird, müssen wir dennoch auf unerwartete Verhaltensweisen bei der Änderung von Objekten achten. Andererseits ist die missbräuchliche Verwendung von Klonen von Werten nicht immer der richtige Weg, und eine falsche Verwendung hat mehr Nachteile als Vorteile. Der richtige Weg, den Datenfluss zu planen, besteht darin, zu überlegen, was man braucht und auf welche möglichen Hindernisse man bei der Implementierung der Anwendungslogik stoßen kann.
Lesen Sie mehr: