Startseite / Artikel / Sieben eingebettete JavaScript-Funktionen, die Abhängigkeiten von Hilfsbibliotheken ersetzen

Sieben eingebettete JavaScript-Funktionen, die Abhängigkeiten von Hilfsbibliotheken ersetzen

Wie Temporal mithilfe von Deklarationen, Map-Upserts, Math.sumPrecise, Iterator-Hilfsfunktionen, Set-Methoden und Promise.withResolvers üblichen JavaScript-Boilerplate entfernt.

2067 Wörter

Früher bedeutete das Starten eines ernsthaften JavaScript-Projekts, eine Datenbibliothek, ein Sammelwerkzeug sowie einen Gruppierungshilfsprogramm zu installieren und anschließend jede geladene Ressource in schützende try...finally-Blöcke einzubetten. In den letzten Release-Zyklen sind eine Reihe von TC39-Vorschlägen, über die jahrelang diskutiert wurde, nun Teil des Standards und finden ihren Weg in immer weiter verbreitete Browser, Node.js, Bun sowie Deno. Diese Anleitung geht auf sieben dieser Vorschläge ein, das jeweilige Problem, das sie lösen, sowie auf Dinge, die man beachten sollte, bevor man eine Abhängigkeit löscht.

Eine Hinweis zur Unterstützung vor dem Start: Diese Funktionen erreichten die Engines zu unterschiedlichen Zeiten, wobei einige noch sehr neu sind. Überprüfen Sie die aktuellen Kompatibilitätsdaten für Ihre Zielbrowser und Laufzeitversionen und verwenden Sie einen Polyfill oder die alte Bibliothek, sofern die Unterstützung noch nicht garantiert ist.

Temporal: eine echte API für Datum und Zeit

Jeder, der das eingebaute Date-Objekt länger als ein paar Minuten verwendet hat, ist auf seine Fallstricke gestoßen:

  • Die Monate sind zero-basiert (0 ist Januar und 11 ist Dezember), während die Tage im Monat bei 1 beginnen.
  • Date-Instanzen sind veränderlich, wodurch die Übermittlung an eine Hilfsfunktion den Wert des Aufrufers stillschweigend ändern kann.
  • Die Umrechnung von Zeitzonen sowie die Berechnungen im Zusammenhang mit Sommerzeit erfordern umständliche manuelle Rechenoperationen oder Bibliotheken wie date-fns, Day.js oder Luxon.

Temporal ist eine neue, unveränderliche API für Datum und Zeit, die entwickelt wurde, um Date zu ersetzen. Es entfernt Date nicht aus der Sprache, da bestehender Code weiterhin funktioniert, aber neuer Code benötigt es nicht mehr.

Getrennte Typen für getrennte Konzepte

Die entscheidende Gestaltungsentscheidung von Temporal besteht darin, unterschiedliche Konzepte als verschiedene Typen abzubilden:

  • Temporal.PlainDate: ein Kalendertag ohne Zeitangabe und Zeitzone, wie zum Beispiel ein Geburtstag.
  • Temporal.PlainTime: eine Uhrzeit wie auf einer Wanduhr ohne Datumangabe, wie zum Beispiel ein Wecker um 07:00.
  • Temporal.ZonedDateTime: ein genauer Zeitpunkt, der mit einer bestimmten Zeitzone und einem Kalender verbunden ist.

Der folgende Codeausschnitt liest die aktuelle Zeit in New York, fügt 14 Tage zu einem einfachen Datum hinzu und misst den Unterschied zwischen zwei Datumsangaben in Monaten und Tagen. Beachten Sie, dass der Unterschied mit largestUnit: 'month' angefordert wird, wodurch das Ergebnis in Monaten plus Tagen angegeben wird und nicht als reine Tagesanzahl.

// 1. Getting current time in a specific time zone
const meetingTime = Temporal.Now.zonedDateTimeISO('America/New_York');
console.log(meetingTime.toString());
// e.g. "2026-09-04T12:09:28-04:00[America/New_York]"

// 2. Safe, readable date math
const deadline = Temporal.PlainDate.from('2026-09-01');
const followUp = deadline.add({ days: 14 });

console.log(followUp.toString()); // "2026-09-15"

// 3. Calculating the exact difference between two dates
const start = Temporal.PlainDate.from('2026-01-01');
const end = Temporal.PlainDate.from('2026-09-04');
const diff = start.until(end, { largestUnit: 'month' });

console.log(`${diff.months} months and ${diff.days} days`);
// "8 months and 3 days"

Jedes Temporal-Objekt ist unveränderlich. Die Aufrufung von add() ändert das ursprüngliche Objekt niemals; es wird ein neues Objekt zurückgegeben. Für viele Projekte bedeutet das, dass eine Datenbibliothek außerhalb des Pakets bleiben kann. Bevor Sie migrieren, lesen Sie häufige Temporal-Falle durch.

Ausdrückliche Ressourcenverwaltung mit using

Eine häufige Ursache für Lecks und blockierte Sperrmechanismen ist es, zu vergessen, etwas zu schließen, was man geöffnet hat:

  • eine Dateihandlung, die nach dem Lesen weiterhin offen bleibt
  • eine Datenbankverbindung, die hängen bleibt, wenn eine API-Route Fehler auslöst
  • Event-Listener oder Worker-Threads, die niemals abgekoppelt werden

Die traditionelle Lösung ist try...finally. Bei drei Ressourcen in einer Funktion verwandelt sich das schnell in stark verschachtelten Standardcode. Eine explizite Ressourcenverwaltung beinhaltet die using-Deklaration zusammen mit zwei bekannten Symbolen, nämlich Symbol.dispose und Symbol.asyncDispose.

Wie die Freigabe ausgelöst wird

Wenn ein Wert mit using deklariert wird, ruft der Engine seine Methode [Symbol.dispose]() auf, sobald die Ausführung den umschließenden Block verlässt – egal ob der Block normal endet oder weil ein Fehler aufgetreten ist. Wenn mehrere Ressourcen im selben Scope deklariert sind, werden sie in umgekehrter Reihenfolge der Deklaration freigegeben, genau wie es verschachtelte try...finally-Blöcke getan hätten.

In dem Beispiel implementiert DatabaseSession den Aufräum-Hook, und runReport deklariert eine Sitzung mit using. Die Reihenfolge der Protokollierung zeigt, dass die Aufräumarbeiten nach der Abfrage und dem Verlassen der Funktion ausgeführt werden:

// Define a resource that knows how to clean itself up
class DatabaseSession {
  constructor(dbName) {
    this.connection = `Connected to ${dbName}`;
    console.log("Database opened.");
  }

  query(sql) {
    return `Results for: ${sql}`;
  }

  // Built-in disposal hook
  [Symbol.dispose]() {
    console.log("Database connection closed automatically!");
  }
}

// Using the resource
function runReport() {
  using session = new DatabaseSession("AnalyticsDB");

  const data = session.query("SELECT * FROM metrics");
  console.log(data);

  // When runReport finishes (or if it throws),
  // session[Symbol.dispose]() runs immediately.
}

runReport();
// Logs:
// 1. "Database opened."
// 2. "Results for: SELECT * FROM metrics"
// 3. "Database connection closed automatically!"

Für asynchrone Aufräumarbeiten, wie zum Beispiel das Schließen eines Netzwerk-Sockets, sollte [Symbol.asyncDispose]() implementiert werden und die Ressource mit await using innerhalb einer asynchronen Funktion deklariert werden. Der Laufzeitumgebung wird anschließend die Aufräumarbeiten abgewartet, bevor sie fortfährt.

Map Upserts mit getOrInsert und getOrInsertComputed

Map ist die richtige Struktur für schlüsselbasierte Sammlungen, doch das Muster „lesen oder bei Fehlen erstellen“ war stets umständlich. Das Gruppieren von Werten unter einem Schlüssel sieht in der Regel so aus:

// The old way: multiple lookups and manual branching
if (!userCache.has(userId)) {
  userCache.set(userId, fetchDefaultProfile(userId));
}
const profile = userCache.get(userId);

Das bedeutet zwei Abfragen (has gefolgt von get) plus eine Bedingungsprüfung, nur um sicherzustellen, dass ein Eintrag vorhanden ist. Die neuen Methoden vereinen das in einen einzigen Aufruf und eine einzige Abfrage:

  • map.getOrInsert(key, defaultValue): gibt den vorhandenen Wert für key zurück oder speichert defaultValue unter diesem Schlüssel und gibt ihn weiter.
  • map.getOrInsertComputed(key, callback): dasselbe Prinzip, doch der Callback wird nur ausgeführt, wenn der Schlüssel fehlt, sodass teure Standardwerte niemals unnötig erstellt werden.

Der Unterschied ist für das obige Beispiel wichtig. getOrInsert(userId, fetchDefaultProfile(userId)) würde bei jedem Zugriff fetchDefaultProfile aufrufen, da die Funktionsargumente vor dem Aufruf ausgewertet werden; die berechnete Variante vermeidet das. Der folgende Codeausschnitt gruppiert Ereignisse nach Benutzeridentifikatoren und erstellt nur beim ersten Auftauchen eines Benutzers ein leeres Array:

const userActivity = new Map();

// Grouping events under user IDs
function logEvent(userId, eventName) {
  // If userId doesn't exist, create an empty array, insert it, and return it.
  const events = userActivity.getOrInsertComputed(userId, () => []);
  events.push(eventName);
}

logEvent("user_42", "login");
logEvent("user_42", "clicked_button");

console.log(userActivity.get("user_42"));
// ['login', 'clicked_button']

Math.sumPrecise für genaue Gesamtbeträge

JavaScript-Zahlen folgen dem IEEE 754-Doppelpräzisions-Fließkommensystem, was das berühmte Ergebnis unten erzeugt:

0.1 + 0.2; // 0.30000000000000004

In Rechnungen, Warenkörben oder Diagrammaggregationen häufen sich solche Fehler, bis der Gesamtbetrag offensichtlich falsch ist.

Math.sumPrecise() nimmt eine iterierbare Menge von Zahlen entgegen und berechnet deren Summe, als gäbe es unbegrenzte Präzision, wobei nur am Ende einmal gerundet wird. Ein naiver reduce rundet nach jedem Schritt, wodurch die Abweichung entsteht. Im Beispiel ergeben sich mit reduce aus den vier Rechnungsposten insgesamt 0.9999999999999999, während Math.sumPrecise 1 zurückgibt (die Konsole gibt 1 aus, nicht 1.0):

const invoiceItems = [0.1, 0.2, 0.3, 0.4];

// Old reduce approach:
const naiveTotal = invoiceItems.reduce((acc, n) => acc + n, 0);
console.log(naiveTotal);
// 0.9999999999999999

// New Math.sumPrecise approach:
const accurateTotal = Math.sumPrecise(invoiceItems);
console.log(accurateTotal);
// 1.0

Es ist wichtig, die Grenzen zu verstehen. Math.sumPrecise beseitigt die angesammelte Abrundungsfehler, kann aber nicht ändern, dass Werte wie 0.1 in Binärform nicht exakt dargestellt werden können. Die Summe von [0.1, 0.2] ergibt weiterhin 0.30000000000000004, weil die genaue Summe dieser beiden Doubles auf diesen Wert abgerundet wird. Das ist eine echte Verbesserung für Statistiken und Dashboards; bei Geldbeträgen bleiben Ganzzahlen in kleineren Einheiten (Cent) oder eine Dezimalbibliothek die sicherere Wahl.

Iterator-Hilfsfunktionen und Iterator.concat

Schon seit über zehn Jahren bieten Arrays die Methoden .map(), .filter() und .slice(), während Iteratoren und Generatoren, wie z. B. unendliche Sequenzen oder Datenbank-Cursors, nichts Vergleichbares hatten. Um .map() auf einem Generator anzuwenden, der Tausende von Zeilen liefert, musste man alles zunächst mit [...generator()] in ein Array umwandeln, was den Vorteil des Speichereinsatzes durch Streaming zunichtemacht.

Iterator-Hilfsfunktionen fügen Iteratoren direkt Methoden wie map, filter, take, drop und flatMap hinzu, und Iterator.concat() verbindet mehrere Iteratoren zu einem. Alle sind träge: Ein Element wird nur dann berechnet, wenn der Verarbeiter danach fragt.

Das Beispiel verknüpft Hilfsfunktionen mit einem endlosen Generator. Da die Auswertung verzögert erfolgt und take(3) nach drei Werten aufhört, endet der Loop trotz dessen, dass die Quelle niemals endet:

function* infiniteCounter() {
  let count = 1;
  while (true) {
    yield count++;
  }
}

// Grab an iterator from our infinite generator
const stream = infiniteCounter()
  .filter(num => num % 2 === 0)   // Keep even numbers
  .map(num => `Count: ${num}`)     // Format them
  .take(3);                        // Stop after 3 values

for (const item of stream) {
  console.log(item);
}
// Logs:
// "Count: 2"
// "Count: 4"
// "Count: 6"

Um mehrere Sequenzen ohne Erstellung eines Zwischenspeichers zusammenzufügen, geben Sie sie an Iterator.concat() weiter. Dieser verarbeitet nacheinander jeden Iterator:

const firstBatch = [1, 2, 3].values();
const secondBatch = [4, 5, 6].values();

const combined = Iterator.concat(firstBatch, secondBatch);

console.log([...combined]); // [1, 2, 3, 4, 5, 6]

Iterator-Hilfsfunktionen stehen bereits seit längerer Zeit in den wichtigsten Engines zur Verfügung; Iterator.concat() ist hingegen neuer, daher sollte man diesen separat überprüfen.

Eigene Set-Operationen

Lange Zeit bot ein Set kaum mehr als .has(), .add() und .delete(). Für Operationen wie die Schnittmenge musste man auf Arrays umwandeln und diese manuell filtern:

// The old manual way:
const intersection = new Set([...setA].filter(x => setB.has(x)));

Set.prototype umfasst nun echte Mengenalgebra: union, intersection, difference, symmetricDifference sowie die Prädikate isSubsetOf, isSupersetOf und isDisjointFrom. Jede Operation gibt eine neue Set-Instanz zurück und lässt die ursprünglichen Mengen unverändert.

In dem Beispiel werden zwei Rollensätze verglichen, um nur die Rechte herauszufiltern, die ausschließlich Admins haben, und es wird überprüft, ob die Rollen des Editors eine Teilmenge der Admin-Rollen sind:

const adminRoles = new Set(['read', 'write', 'delete', 'audit']);
const editorRoles = new Set(['read', 'write']);

// Find privileges exclusive to admins
const adminOnly = adminRoles.difference(editorRoles);
console.log([...adminOnly]); // ['delete', 'audit']

// Check role containment
console.log(editorRoles.isSubsetOf(adminRoles)); // true

Sie überspringen außerdem die temporären Arrays, die der Ansatz mit Spread- und Filterfunktionen erzeugt.

Promise.withResolvers für verzögerte Promises

Manchmal muss ein Promise außerhalb seines Konstruktors abgewickelt werden, beispielsweise durch einen Ereignishandler oder eine anderswo registrierte Callback-Funktion. Dieses „aufgeschobene“ Muster erforderte früher, dass Variablen aus dem Ausführungskontext herausgehoben wurden:

// The clunky way
let resolveFn, rejectFn;
const myPromise = new Promise((res, rej) => {
  resolveFn = res;
  rejectFn = rej;
});

Promise.withResolvers() gibt das Promise zusammen mit seinen resolve- und reject-Funktionen in einem einzigen Objekt zurück. Im Beispiel löst ein Klickhandler das Promise auf, während anderer Code einfach darauf wartet. Beachten Sie, dass der Auszug Top-Level-await-Anweisungen verwendet, weshalb er in einem ES-Modul oder innerhalb einer async-Funktion ausgeführt werden muss:

const { promise, resolve, reject } = Promise.withResolvers();

// Attach your listeners or pass the resolve handle to an event handler
document.getElementById("submit-btn").addEventListener("click", () => {
  resolve("User clicked submit!");
}, { once: true });

// Await the promise wherever you need it
const message = await promise;
console.log(message);

Dieses Muster ist nützlich für benutzerdefinierte Ereigniswarten, Brücken zu Web Workers sowie zur Anpassung von auf Callbacks oder Emittern basierenden APIs an async-Code.

Was das für Ihre Abhängigkeiten bedeutet

Der gemeinsame Nenner ist die Parität: Die Sprache umfasst nun routinemäßige Aufgaben wie das Arbeiten mit Datumsangaben, die Bereinigung von Ressourcen, gruppenbasierte Verarbeitung sowie Mengenrechnungen, die früher an Bibliotheken im Userland übergeben wurden. Bevor Sie im nächsten Sprint ein weiteres Paket hinzufügen, prüfen Sie Ihre Ziele:

  • Falls Sie für moderne Browser oder aktuelle Node.js-, Bun- oder Deno-Versionen entwickeln, existieren viele dieser Tools bereits global.
  • Jede vermiedene Abhängigkeit bedeutet ein kleineres Bundle, eine schnellere Startzeit sowie Code, den jeder JavaScript-Entwickler lesen kann, ohne eine Drittanbieter-API erlernen zu müssen.

Kernpunkte

  • Temporal bietet für Datumsangaben unveränderliche, klar getrennte Typen; er ersetzt Date in neuem Code, ohne diesen zu entfernen.
  • using und await using sorgen für automatische und geordnete Bereinigung und ersetzen damit verschachtelte try...finally-Blöcke.
  • Verwenden Sie getOrInsertComputed, wenn der Standardwert teuer zu erstellen ist.
  • Math.sumPrecise behebt eine angesammelte Rundungsfehlerentwicklung, nicht die binäre Darstellung; halten Sie Geldwerte in Ganzzahlen.
  • Hilfsfunktionen für Iteratoren und Set-Methode beseitigen ganze Kategorien von Array-Umwandlungen.
  • Überprüfen Sie die Laufzeitunterstützung für jede Funktion, da diese Vorschläge in sehr unterschiedlichen Zeitplänen umgesetzt wurden.
  • Zusätzliche Literatur