Startseite / Artikel

Überprüfung von utils.js: Sechs eingebaute Funktionen, die manuell erstellte Hilfsfunktionen ersetzen

Erfahren Sie, welche nativen JavaScript-APIs Hacke zur tiefen Klonierung, defensiv angelegte Kopien, manuelle Zeitlimits, verzögerte Promises sowie lodash groupBy ersetzen können und welche Einschränkungen dabei gelten.

Verlag: REACTAPP.TOP5 Min. Lesezeit1052 Wörter

Quellen: 6 Modern JavaScript Features You’re Probably Still Working Around

Viele Codebasen enthalten weiterhin Hilfsfunktionen sowie Bibliothekseinbindungen für Probleme, die die Plattform bereits vor Jahren gelöst hat. Diese Ergänzungen fanden stillschweigend statt, sodass sich um die alten Workarounds gebildete Gewohnheiten niemals überprüft wurden. Im Folgenden sind sechs eingebaute Funktionen aufgeführt, gegen die Sie Ihren utils.js überprüfen sollten – zusammen mit den Fallstricken, die jede von ihnen beseitigt, sowie den Einschränkungen, die Sie kennen sollten, bevor Sie etwas löschen.

structuredClone() anstelle des JSON-Rundwegs

Die gängigste selbst entwickelte Methode zur tiefen Kopie serialisiert ein Objekt und parsiert es anschließend wieder:

const copy = JSON.parse(JSON.stringify(original));

Sie versagt bei allem, was JSON nicht darstellen kann: Date-Instanzen werden als Strings zurückgegeben, Eigenschaften, die auf undefined gesetzt sind, werden weggelassen, Map- und Set-Objekte verwandeln sich ohne Warnung in leere Objekte, und zyklische Referenzen verursachen, dass JSON.stringify einen Fehler auslöst.

structuredClone() ist eine globale Funktion, die in modernen Browsern und Node.js verfügbar ist und nicht importiert werden muss. Nehmen wir ein Objekt, das einen Datumswert, ein Set sowie ein Map enthält:

const original = {
  date: new Date(),
  items: new Set([1, 2, 3]),
  meta: new Map([['key', 'value']]),
};

Durch das Klonen bleiben alle diese Datentypen unverändert, und zyklische Strukturen werden ebenfalls korrekt kopiert:

const copy = structuredClone(original);
// Dates, Sets, Maps, and circular refs all survive intact

Es verwendet den strukturierten Klonalgorithmus hinter postMessage und IndexedDB, wodurch auch Randfälle abgedeckt werden, die bei manuell erstellten Klonen übersehen werden. Es gibt jedoch Einschränkungen: Funktionen und DOM-Node können nicht geklont werden, wodurch ein Fehler auftritt, und Instanzen eigener Klassen werden als einfache Objekte ohne ihr Prototyp zurückgegeben.

toSorted() und toReversed() anstelle von defensiven Kopien

.sort() und .reverse() ändern die Daten vor Ort, wodurch das Sortieren eines über einen Referenzzeiger geteilten Arrays es überall verändert. Ausgehend von diesem Array:

const scores = [30, 10, 20];

toSorted() gibt ein neues, sortiertes Array zurück und lässt das ursprüngliche Array unverändert:

const sorted = scores.toSorted((a, b) => a - b);console.log(scores); // [30, 10, 20] — untouched
console.log(sorted);  // [10, 20, 30]

toReversed() und toSpliced() folgen dem gleichen Muster des Zurückgeben von Kopien. Wenn Ihr Code voller [...arr].sort()-Anweisungen ist, um den Zustand unveränderlich zu halten – wie es bei React Reducern oft der Fall ist –, ist das Spreading dann nicht mehr notwendig. Eine ausführlichere Betrachtung dieser Methoden, die Kopien zurückgeben, finden Sie in modernen JavaScript-Array-Methoden, die gängige Boilerplate-Patterns ersetzen.

Object.hasOwn() anstelle von hasOwnProperty

Durch das Aufrufen der Methode über das Objekt wird eine Falle verborgen:

obj.hasOwnProperty('key'); // breaks if obj = Object.create(null)

Ein mit Object.create(null) erstelltes Objekt hat kein Prototyp, weshalb es niemals hasOwnProperty erbt und der Aufruf einen TypeError auslöst. Dasselbe passiert, wenn ein Objekt eine eigene Eigenschaft mit diesem Namen definiert. Object.hasOwn() ist eine statische Funktion, sodass sie nicht davon abhängt, was das Objekt erbt:

Object.hasOwn(obj, 'key'); // works regardless of the object's prototype chain

Der Tausch ist unbedeutend, verhindert aber Fehler, die erst in der Produktion auftreten, wenn ein Aufrufer ein Objektformat übermittelt, das nicht vorgesehen war.

AbortSignal.timeout() anstelle eines manuellen Timers

Um fetch eine Frist zu geben, war früher ein AbortController zusammen mit einem setTimeout erforderlich, der ihn auslöst:

const controller = new AbortController();
const id = setTimeout(() => controller.abort(), 5000);

Dann musste man sich daran erinnern, den Timer zu löschen, sobald die Anfrage abgeschlossen war:

fetch(url, { signal: controller.signal }).finally(() => clearTimeout(id));

Das Signal kann nun seine eigene Frist enthalten:

fetch(url, { signal: AbortSignal.timeout(5000) });

Eine Zeile, keine Aufräumarbeiten, kein verbleibender Timer. Wenn dieser auslöst, wird die Anfrage mit einem TimeoutError DOMException abgelehnt, was es ermöglicht, einen Timeout von einem vom Benutzer ausgelösten Abbruch zu unterscheiden. Um einen Timeout mit einer anderen Abbruchquelle zu kombinieren, siehe AbortSignal.any() in der aktuellen Dokumentation.

Promise.withResolvers() anstelle von leakenden resolve- und reject-Funktionen

Mannchmal muss ein Promise von außerhalb seines Executors abgeschlossen werden. Die klassische Workaround-Lösung besteht darin, Variablen zu deklarieren und sie innerhalb des Konstruktor-Callbacks zuzuweisen:

let resolve, reject;
const promise = new Promise((res, rej) => {
  resolve = res;
  reject = rej;
});

Das funktioniert, doch das Hineinschmuggeln von Funktionen aus einer Schließung ist leicht missverständlich. Promise.withResolvers() gibt das Promise sowie beide Abschlussfunktionen zusammen zurück:

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

Das passt natürlich, wenn Sie asynchrone Aufgaben in eine Warteschlange geben oder eine ereignisgesteuerte API in Promises einhüllen. Für ein umfassenderes Beispiel siehe die Koordination von LLM-Toolaufrufen mit Promise.withResolvers.

Object.groupBy() anstelle von lodash groupBy

Das Gruppieren von Datensätzen nach einem Schlüssel ist einer der häufigsten Gründe, warum man eine Hilfsbibliothek importiert. Angenommen, es gibt eine Liste von Personen:

const people = [
  { name: 'Sam', age: 21 },
  { name: 'Lee', age: 34 },
  { name: 'Kai', age: 21 },
];

Object.groupBy() nimmt ein iterierbares Objekt und eine Callback-Funktion entgegen und gibt ein Objekt zurück, das für jeden Schlüssel einen Array enthält:

const byAge = Object.groupBy(people, (p) => p.age);
// { 21: [Sam, Kai], 34: [Lee] }

Achten Sie auf den Namen. Ein früher Vorschlag nannte dies Array.prototype.group(), wurde jedoch vor der Standardisierung umbenannt und in die statischen Methoden Object.groupBy() (sowie Map.groupBy(), wenn die Schlüssel keine Zeichenketten sind) verschoben. Nur die statischen Formen werden mitgeliefert. Das Ergebnis weist ein null-Prototyp auf, und es handelt sich um die neueste API in dieser Liste – überprüfen Sie daher vor dem Entfernen der Abhängigkeit Ihre minimale unterstützte Browser- und Node.js-Version.

Vor dem Löschen der Hilfsfunktionen

Keiner dieser Schritte ändert Ihre Architektur, doch jeder beseitigt ein fehleranfälliges Muster oder eine unnötige Abhängigkeit. Eine sinnvolle Reihenfolge:

  • Überprüfen Sie die Unterstützung auf caniuse.com oder MDN im Vergleich zu den Umgebungen, die Sie tatsächlich ansprechen möchten – einige dieser Funktionen sind neuere als andere, und ältere Browser verfügen möglicherweise nicht darüber.
  • Behalten Sie Polyfills bei, bis Ihre Unterstützungsmatrix tatsächlich ihr Entfernen zulässt.
  • Ersetzen Sie die Aufrufstellen schrittweise, wobei Tests zu Kloning und Gruppierung durchgeführt werden sollten, wenn die Ausgabestruktur von Bedeutung ist.
  • Entfernen Sie die Importe der alten Hilfsfunktionen oder Bibliotheken erst, wenn nichts mehr darauf verweist.