ECMAScript 2023/24 – Welche Änderungen sind geplant?
Willkommen in der Zukunft von JavaScript: Im Juli letzten Jahres haben wir einige fantastische Neuerungen bekommen, die die JavaScript-Landschaft bereichern. Wir möchten dir heute einen lockeren und verständlichen Einblick in die aktuellen Funktionen sowie in die spannenden Features geben, die gerade die finale Phase des TC39-Komitees durchlaufen. Lass uns zudem gemeinsam einen Blick auf das werfen, was uns noch im Laufe des Jahres im ECMAScript 2024 erwartet. Für alle JavaScript-Fans gibt es jede Menge Spannendes zu entdecken!
Neue Funktionen und Verbesserungen
Symbole als WeakMap-Schlüssel
„Klein, aber nützlich“ – so könnte man die neueste Ergänzung in JavaScript zusammenfassen. Seit der Einführung in ECMAScript 2015 ermöglichen WeakMaps das Erweitern von Objekten mit zusätzlichen Eigenschaften – beispielsweise zur Nachverfolgung der Nutzungshäufigkeit eines Objekts – ohne das Risiko eines Speicherlecks, da die Schlüssel-Wert-Paare in einer WeakMap vom Garbage Collector “recycled” werden können.
Ursprünglich konnten in WeakMaps nur Objekte als Schlüssel verwendet werden, aber man möchte, dass die Schlüssel einzigartig sind. „Symbole wurden als eine neue unveränderliche Methode definiert, die nicht neu erstellt werden kann. Daher macht es viel mehr Sinn, diese als einzigartigen Schlüssel in der WeakMap zu haben“, sagte Chris Heilmann, Entwicklungsadvokat und Browser-Ingenieur.
Code Beispiel:
const weakMap = new WeakMap(); const key = Symbol("key1"); weakMap.set(key, "Welcome Symbols to WeakMap !"); console.log(weakMap.get(key)); // Welcome Symbols to WeakMap !
Verbesserungen bei Arrays
Auch bei Arrays wurden einige Neuerungen hinzugefügt. In ES2023 wurden .findLast( ) und .findLastIndex( ) eingeführt, um die Effizienz beim Suchen in Arrays zu steigern, insbesondere wenn das gesuchte Element am Ende des Arrays vermutet wird. Zudem ermöglichen neue Methoden wie .toReversed( ), .toSorted( ), .toSpliced( ) und .with( ) die Manipulation von Arrays, ohne das Original zu verändern (siehe Call-by-Reference vs. Call-by-Value).
Change Array by Copy
Reverse Sequence
const sequence = [1, 2, 3, 4]; const reversed = sequence.toReversed(); console.log(reversed); // [4, 3, 2, 1] console.log(sequence); // [1, 2, 3, 4]
Sort Array
const unsorted = [1, 3, 4, 2]; const sorted = unsorted.toSorted(); console.log(sorted); // [1, 2, 3, 4] console.log(unsorted); // [1, 3, 4, 2]
Verwendung von .findLast() und .findLastIndex()
/* Beispiel vor ES2023 */ const numbers = [42, 13, 7, 21, 82, 100]; [...numbers].reverse().find(n => n === 82); // 82, iteriert von links nach rechts numbers.length - 1 - [...numbers].reverse().findIndex(n => n === 82); // 4 /* Beispiel nach ES2023 */ numbers.findLast(n => n === 82); // 82, iteriert von rechts nach links numbers.findLastIndex(n => n === 82); // 4
Hashbang-Kommentare
Die bereits von SEO bekannten Hashbangs (#!), welche zur Indexierung für Crawlern verwendet werden, bestimmen nun standardisiert den Interpreter für JavaScript-Skripte. Dies erleichtert die Ausführung von JS-Skripten als eigenständige ausführbare Dateien.
Beispiel:
#!/usr/bin/env node console.log('Hello, World!');
Ausblick auf ES2024
- Well-Formed Unicode Strings
- Regular Expression v-Flag: Erweiterte Möglichkeiten für reguläre Ausdrücke.
- Atomics.waitAsync: Asynchrones Warten auf Speicherorte im Shared Memory.
- Promise.withResolvers
- Array Grouping
- Temporal: Moderne API für Datum und Zeit
Well-Formed Unicode Strings
In der Welt von JavaScript sind Strings nach dem UTF-16-Standard mit Surrogate-Paaren kodiert. Hast du schon einmal von diesem Konzept? Falls nicht hier eine kurze Erklärung:
- Surrogat-Paare: Da manche Unicode-Zeichen nicht in 16 Bit passen, verwendet UTF-16 für diese Zeichen zwei 16-Bit-Codes, bekannt als Surrogat-Paare. Ein Surrogat-Paar besteht aus einem „High-Surrogate“ und einem „Low-Surrogate“. Zusammen repräsentieren diese beiden Surrogat-Codes ein einzelnes Unicode-Zeichen, das außerhalb des Bereichs der sogenannten „Basic Multilingual Plane“ liegt, also ein Zeichen, das mehr als 16 Bit zur Darstellung benötigt.
Schauen wir uns nun die neue Funktion isWellFormed( ) an. Diese bietet eine praktische Möglichkeit, die Wohlgeformtheit eines Strings zu überprüfen, indem sie sicherstellt, dass keine isolierten Surrogate vorhanden sind. Sie zeichnet sich durch ihre Effizienz aus, da sie es ermöglicht, direkt auf die interne String-Darstellung zuzugreifen, was gegenüber eigenen Implementierungen ein deutlicher Vorteil ist. Für die Umwandlung eines Strings in einen wohlgeformten String steht die Methode .toWellFormed() zur Verfügung. Zudem bietet .isWellFormed() die Flexibilität, fehlerhafte Strings – also solche, die nicht den Kriterien entsprechen – auf andere Weise zu behandeln. Dies ist besonders bei der encodeURI() Funktion nützlich, da diese nur wohlgeformte Strings akzeptiert.
Eine mögliche Implementierung könnte so aussehen:
const str1 = "ab\uD800"; const str2 = "abc"; console.log(str1.isWellFormed()); // false console.log(str2.isWellFormed()); // true console.log(str1.toWellFormed()); // "ab�" console.log(str2.toWellFormed()); // "abc"
Beispiel mit encodeURI()
const problematicURL = "https://clickstorm.de/query=\uDC00data"; try { encodeURI(problematicURL); } catch (e) { console.log('Error:', e.message); // Erwarted: URIError: URI malformed } // Verwendung von toWellFormed() console.log('Well Formed URI:', encodeURI(problematicURL.toWellFormed())); // Erwartete Ausgabe: "https://clickstorm.de/query=%EF%BF%BDdata"
Atomics.waitAsync
Es gibt auch eine Erneuerung bei der asynchronen Programmierung. Die statische Methode Atomics.waitAsync( ) wartet asynchron auf einen gemeinsamen Speicherort und gibt ein Promise zurück. Im Gegensatz zu Atomics.wait( ) ist waitAsync nicht-blockierend und kann im Haupt-Thread verwendet werden. Stell dir vor, du arbeitest an einer Webanwendung, die gleichzeitig viele Daten verarbeiten muss. Mit waitAsync kannst du effizient auf bestimmte Ereignisse in gemeinsamen Speicherbereichen warten, ohne dass der Nutzerfluss deiner Anwendung unterbrochen wird. Es ist wie ein zuverlässiger Assistent, der im Hintergrund arbeitet, während du dich auf andere wichtige Aufgaben konzentrierst. Sehen wir uns nun die Verwendung von waitAsync( ) an:
Stell dir vor, wir haben eine Art digitales Schachbrett, das SharedArrayBuffer genannt wird. Auf diesem Schachbrett gibt es eine Reihe von Feldern, die durch Int32Array repräsentiert werden. Jedes Feld kann eine Zahl enthalten. In unserem Beispiel haben wir dieses Schachbrett mit 1024 Feldern erstellt, aber wir konzentrieren uns nur auf das erste Feld (Position 0). Zunächst setzen wir den Wert dieses Feldes auf 0.
const sab = new SharedArrayBuffer(1024); const int32 = new Int32Array(sab);
Jetzt kommt der spannende Teil: Wir haben einen Beobachter, den „lesenden Thread“. Dieser Beobachter ist besonders interessiert am ersten Feld. Er möchte wissen, ob und wann sich der Wert dieses Feldes ändert. Also setzt er sich daneben und beginnt zu warten. Aber statt die ganze Zeit wach zu bleiben, nutzt er Atomics.waitAsync() und sagt im übertragenen Sinne: „Ich warte bis zu 1000 Millisekunden (1 Sekunde), um zu sehen, ob der Wert von 0 auf etwas anderes wechselt.“
const result = Atomics.waitAsync(int32, 0, 0, 1000); // { async: true, value: Promise {<pending>} }
Atomics.waitAsync() ist clever. Es hält den Beobachter nicht davon ab, andere Dinge zu tun, während er wartet. Es gibt ihm ein Promise zurück und benachrichtigt diesen, sobald sich etwas ändert, oder die Zeit abgelaufen ist.
Atomics.notify(int32, 0); // { async: true, value: Promise {<fulfilled>: 'ok'} }
Fazit: Wenn das Promise nicht zu „ok“ aufgelöst wird, war der Wert am gemeinsam genutzten Speicherort nicht wie erwartet (der Wert wäre „not-equal“ anstelle eines Promises), oder das Zeitlimit wurde erreicht (das Promise wird mit „time-out“ aufgelöst).
Array Gruppierungen
Das „Array Grouping“-Feature, zielt darauf ab, das Gruppieren von Elementen in Arrays (und iterierbaren Objekten) zu vereinfachen.
Codebeispiele:
Object.groupBy
Zweck: Ermöglicht das Gruppieren von Elementen eines Arrays basierend auf einem beliebigen Schlüssel.
const array = [1, 2, 3, 4, 5]; const grouped = Object.groupBy(array, (num) => { return num % 2 === 0 ? 'even' : 'odd'; }); // Ergebnis: { odd: [1, 3, 5], even: [2, 4] }
Map.groupBy
Zweck: Ähnlich wie Object.groupBy, aber gibt die Ergebnisse in einer Map zurück, was nützlich ist, wenn die Schlüssel komplexe Objekttypen sind.
const odd = { odd: true }; const even = { even: true }; const mapGrouped = Map.groupBy(array, (num) => { return num % 2 === 0 ? even : odd; }); // Ergebnis: Map { {odd: true}: [1, 3, 5], {even: true}: [2, 4]
Temporal API
Schon seit langer Zeit hat die Verwendung des Date-Objekts in JS einige Schwierigkeiten. Um diese zu beheben wurde die Temporal-API als ein globales Objekt, das als Top-Level-Namespace (ähnlich wie Math) fungiert und eine moderne API für Datum und Zeit in die ECMAScript-Sprache bringt, hinzugefügt. Dies adressiert die Probleme von Date, indem es:
- Benutzerfreundliche APIs für Datum- und Zeitberechnungen bereitstellt.
- Erstklassige Unterstützung für alle Zeitzonen, einschließlich DST-sicherer Arithmetik, bietet.
- Ausschließlich mit Objekten arbeitet, die feste Daten und Zeiten repräsentieren.
- Einen streng spezifizierten String-Format für das Parsen verwendet.
- Nicht-gregorianische Kalender unterstützt.
Des Weiteren stellt es separate ECMAScript-Klassen für reine Datum-, reine Zeit- und andere spezifische Anwendungsfälle zur Verfügung. Dies verbessert die Lesbarkeit des Codes und vermeidet Fehler, die durch falsche Annahmen über 0, UTC oder die lokale Zeitzone bei tatsächlich unbekannten Werten entstehen.
Erweiterbarkeit von ArrayBuffer / SharedArrayBuffer
Es wird eine Funktion hinzugefügt, um ArrayBuffer und SharedArrayBuffer erweiterbar zu machen, was eine effizientere Speicherverwaltung und bessere Synchronisation mit WebAssembly ermöglicht. Diese Buffer können bei der Erstellung eine maximale Größe erhalten und dann dynamisch innerhalb dieser Grenzen vergrößert oder verkleinert (ArrayBuffer) bzw. nur vergrößert werden (SharedArrayBuffer). Dies verbessert die Leistung vor allem für Anwendungen wie WebGPU, die häufige Änderungen der Buffergröße erfordern.
Beispiel
Zuerst wird ein ArrayBuffer namens rab mit einer anfänglichen Größe von 1024 Bytes erstellt.
Zusätzlich wird eine maximale Größe von 2048 Bytes festgelegt.
let rab = new ArrayBuffer(1024, { maxByteLength: 2048 });
Ein Uint32Array namens uintArray wird erstellt, das rab als Basis verwendet.
Die Länge von uintArray ist 256, da jeder Uint32 4 Bytes groß ist und rab anfänglich 1024 Bytes hat.
let uintArray = new Uint32Array(rab); console.log(uintArray.length); // 256 (1024 Bytes / 4 Bytes pro Uint32)
Der ArrayBuffer rab wird dann auf seine maximale Größe von 2048 Bytes erweitert.
rab.resize(2048); console.log(uintArray.length); // 512 (2048 Bytes / 4 Bytes pro Uint32)
Nach der Vergrößerung des ArrayBuffer rab passt sich die Länge von uintArray entsprechend an.
Sie verdoppelt sich auf 512, da nun mehr Speicherplatz für Uint32-Einträge verfügbar ist.
Vorteile der Speichererweiterbarkeit:
- Flexibilität: Die Möglichkeit, die Größe eines ArrayBuffer dynamisch anzupassen, bietet Flexibilität bei der Speicherverwaltung.
- Speichereffizienz: Vermeidet das Neuerstellen und Kopieren ursprünglich größerer ArrayBuffer-Objekte, um Data Leaks zu reduzieren.
- Performance: Durch das Umgehen von Neuzuweisungen und Kopiervorgängen verbessert sich die Performance, besonders in Anwendungen mit häufigen Speichergrößenänderungen, indem Neuzuweisungen und Kopiervorgänge umgehen.
- Präzisere Speicherverwaltung: Entwickler können den Speicher präziser entsprechend den Anforderungen ihrer Anwendung verwalten, was besonders bei komplexen Datenstrukturen oder leistungsintensiven Anwendungen von Vorteil ist.
Fazit:
Wir haben gesehen, dass das ECMAScript-Update von 2023/24 einige kleine, aber dennoch wirkungsvolle Erneuerungen mit sich bringt. Besonders freuen wir uns auf die neue Zeit und Datums API (Temporal), welche einige der Herausforderungen, die das bisherige Date-Objekt mit sich brachte, lösen soll und das Arbeiten intuitiver gestaltet. Allgemein sind diese Updates ein gutes Zeichen für JavaScript, da die Sprache hierdurch beständig wächst und sich weiterentwickelt. Sie passt sich den Bedürfnissen moderner Web-Entwicklungen an und bleibt damit eine unverzichtbare Sprache in der Welt der Webentwicklung.
Auf welche Änderung freust du dich am meisten? Lass es uns gerne in den Kommentaren wissen!
Bilder: Canva, Medium.com, Chat-GPT / DALL·E 3