Sieben gängige JavaScript-Idiome, die heimlich zukünftige Fehler verursachen
Erklärt, wie alltägliche JavaScript-Muster wie Wahrheitsprüfungen, optionales Chaining und Spread-Syntax Annahmen verbergen, die im Laufe der Weiterentwicklung des Codes stillschweigend zu Fehlern führen.
Ein Pull Request reduziert zwölf Zeilen JavaScript auf vier. Die aktualisierte Version nutzt optionale Kettensuche, eine Wahrheitsprüfung, Objekterweiterung sowie eine übersichtliche Abfrage-Tabelle, sodass die Überprüfung reibungslos verläuft. Die Änderung wirkt straffer, liest sich natürlicher und funktioniert korrekt für alle Eingaben, die die Anwendung derzeit erhält.
Monate später wird 0 als gültiger Konfigurationswert akzeptiert. Eine API-Antwort beginnt, ein Feld wegzulassen, das früher stets enthalten war. Eine Hintergrundaufgabe, die einst Dutzende von Datensätzen verarbeitete, muss plötzlich Tausende bearbeiten. Ein verschachteltes Objekt wird an einer Stelle wiederverwendet, obwohl angenommen wurde, es sei bereits kopiert worden. Kein dieser späteren Ereignisse scheint mit der früheren Refaktorierung zusammenzuhängen, doch jedes von ihnen zeigt eine Annahme auf, die im kompakten Code stillschweigend berücksichtigt wurde.
Genau deshalb sind bestimmte JavaScript-Idiome riskant. Sie wirken selten zu diesem Zeitpunkt falsch, und erfahrene Entwickler greifen darauf zurück, weil sie unter den aktuellen Anforderungen praktisch, ausdrucksstark und präzise sind. Probleme entstehen, wenn diese Bequemlichkeit mehrere unterschiedliche Zustände stillschweigend in ein einziges Verhalten zusammenführt oder wenn eine Syntax, die wie eine Sicherheitsgarantie erscheint, in Wirklichkeit nur eine Abkürzung ist.
Daher lautet die eigentliche Frage bei der Überprüfung nicht, ob ein Codeabschnitt clever oder prägnant ist. Vielmehr geht es darum, ob der Code seine Annahmen klar genug offenlegt, sodass zukünftige Änderungen zu einem deutlichen Fehler führen und nicht zu einem stillschweigend falschen Ergebnis.
1. Truthy-Prüfungen führen zu denselben Entscheidungen bei unterschiedlichen Werten
Stellen Sie sich eine Konfigurationseinstellung vor, die steuert, wie oft eine Operation wiederholt werden darf.
function getRetryCount(config) {
if (!config.retries) {
return 3;
}
return config.retries;
}
Diese Logik funktioniert, solange retries entweder eine positive Zahl ist oder fehlt. Doch sobald jemand die Möglichkeit hinzufügt, Wiederholungsversuche vollständig durch Setzen des Wertes auf 0 zu deaktivieren, überschreibt die Funktion diese explizite Wahl stillschweigend und gibt stattdessen 3 zurück. Die Wahrheitsregeln von JavaScript werden korrekt befolgt – es ist nur so, dass diese Regeln nicht mehr mit den tatsächlichen Anforderungen des Unternehmens übereinstimmen.
Mehrere Werte außer false gelten in JavaScript als falsch: 0, "", null, undefined und NaN fallen alle in diese Kategorie. Daher prüft eine Bedingung wie if (!value) eigentlich nicht, ob etwas fehlt – sie prüft vielmehr, ob JavaScript diesen Wert auf false umwandeln würde, was eine weitaus breitere Frage ist als „Ist dieser Wert abwesend?“
Eine genaueere Version hängt davon ab, wie der tatsächliche Vertrag aussieht.
function getRetryCount(config) {
if (config.retries === undefined) {
return 3;
}
return config.retries;
}
Und wenn sowohl null als auch undefined dazu dienen sollen, anzuzeigen, dass „nichts bereitgestellt wurde“, kann das Nullish-Coalescing die Absicht sauberer ausdrücken.
const retries = config.retries ?? 3;
Diese Unterscheidung ist für Formulareingaben, Paginierungsparameter, Preise, Grenzwerte, Feature-Flags, Zähler und andere numerische Einstellungen von großer Bedeutung. Ein Flag mit dem Namen enabled kann zu Recht den Wert false haben. Ein Suchfeld kann zu Recht eine leere Zeichenkette sein. Ein Offset kann zu Recht den Wert 0 haben. Jeden falschen Wert als „nicht bereitgestellt“ zu betrachten, führt dazu, dass Informationen zugunsten der Kürze weggeworfen werden.
Nichts davon macht Prüfungen auf Wahrheitswert per se falsch. Sie funktionieren gut, wenn die Anforderung tatsächlich auf dem Wahrheitswert beruht – beispielsweise um zu entscheiden, ob ein optionales String-Wert nur angezeigt werden soll, wenn er nicht leer ist. Sie werden zu einem Problem, sobald das Domänenmodell mehr unterschiedliche Zustände aufweist, als die eigene boolesche Zwangsumwandlung in JavaScript bewahren kann.
Die Frage, die bei der Überprüfung gestellt werden sollte, ist einfach: Muss diese Bedingung tatsächlich auf Falschheit prüfen, oder wird der Wahrheitswert lediglich als Ersatz für eine spezifischere Geschäftsregel verwendet?
2. Optional Chaining kann fehlerhafte Daten in stillschweigend fehlende Daten verwandeln
Optional Chaining zählt zu den wertvollsten Erweiterungen in JavaScript, da es das Arbeiten mit tief strukturierten, optionalen Datenstrukturen erheblich erleichtert.
const avatarUrl = user.profile?.avatar?.url;
Wenn profile oder avatar tatsächlich fehlen können, ist dies genau die Situation, für die optionales Kettensuchen existiert. Hier ist es sinnvoll, undefined zurückzubekommen, da das Fehlen ein normales, erwartetes Ergebnis ist.
Probleme entstehen, wenn optionales Kettensuchen auf Daten angewendet wird, die eigentlich immer vorhanden sein sollten.
function getShippingCountry(order) {
return order.customer?.address?.country;
}
Nehmen wir an, der Vertragsrahmen des Systems garantiert, dass jede versandfähige Bestellung einen Kunden sowie eine Versandadresse enthält. Wenn ein Fehler in einer früheren Schicht dazu führt, dass eine Bestellung eines dieser Elemente fehlt, wandelt optionales Kettensuchen stillschweigend das, was eigentlich eine Vertragsverletzung darstellt, in ein gewöhnliches undefined um. Dieses Problem kann erst viel später auftauchen, beispielsweise als falsche Steuerberechnung, fehlendes Versandetikett oder eine verwirrende Abzweigung an einer anderen Stelle im Codebasis.
Ein besseres Vorgehen in diesem Fall könnte darin bestehen, sofort zu fehlen – genau an dem Punkt, an dem die Annahme tatsächlich von Bedeutung ist.
function getShippingCountry(order) {
if (!order.customer?.address) {
throw new Error(
`Order ${order.id} is missing a required shipping address`
);
}
return order.customer.address.country;
}
Diese Version ist ausführlicher, vermittelt aber mehr Informationen. Sie macht jedem, der sie später liest, klar, dass die Adresse keine bloß nützliche Datenmenge ist, die eventuell angezeigt wird oder auch nicht – ihr Fehlen bedeutet, dass das Objekt für diese Operation überhaupt nicht als gültig angesehen werden sollte.
Optional chaining neigt außerdem dazu, sich auszubreiten, sobald Unsicherheiten in einen Typ oder ein API-Vertrag eindringen. Ein einzelnes unsicheres Feld wird zu foo?.bar, was bald zu foo?.bar?.baz wird, und schon tolerieren alle Aufrufer stillschweigend fehlende Daten, anstatt zu fragen, warum ein als erforderlich angesehenes Feld ständig verschwindet. Die Syntax verringert zwar die geworfenen Ausnahmen, kann aber genauso leicht die allgemeine Unklarheit erhöhen.
Ob die optionale Kettensuche das richtige Werkzeug ist, sollte aus dem Domänenmodell abgeleitet werden und nicht aus Bequemlichkeit. Wenn ein Wert tatsächlich optional ist, bietet die optionale Kettensuche eine saubere Möglichkeit, dies auszudrücken. Ist er hingegen erforderlich, kann das Weiterleiten von undefined durch das Programm dazu führen, dass Fehler viel schwieriger zurückverfolgt werden können.
3. Die Spread-Syntax wirkt wie eine vollständige Kopie, doch verschachtelte Objekte behalten ihre ursprüngliche Identität
Das Ausbreiten eines Objekts in ein neues ist eine kompakte Art, Updates zu schreiben, die unveränderlich erscheinen.
const updatedUser = {
...user,
name: "Umar",
};
Auf der obersten Ebene wird tatsächlich ein neues Objekt mit den erwarteten Werten erstellt. Dieser Erfolg verleitet Entwickler oft dazu, { ...object } als „Kopiere dieses Objekt“ zu interpretieren, obwohl die Operation in Wirklichkeit nur die direkt auf diesem Objekt liegenden Eigenschaften dupliziert.
Alles, was um ein Level tiefer eingebettet ist, erhält diese Behandlung nicht.
const original = {
name: "Umar",
preferences: {
theme: "light",
},
};
const copy = { ...original };
copy.preferences.theme = "dark";
console.log(original.preferences.theme);
// "dark"
Hier ist copy tatsächlich ein eigenständiges äußeres Objekt, aber copy.preferences verweist weiterhin auf genau dasselbe Objekt wie original.preferences. Der Spread hat lediglich den Verweis dupliziert, nicht die daran hängende Struktur.
Dies wird im Rahmen der Zustandsverwaltung schnell riskant, da der resultierende Code weiterhin die optische Form einer unveränderlichen Aktualisierung aufweist.
const nextState = { ...state };
nextState.user.preferences.theme = "dark";
Das äußere Objekt ist neu, doch user und preferences können genau die gleichen Objekte aus dem vorherigen Zustand sein. Alles, was entscheidet, ob neu gerendert oder neu berechnet werden soll, basierend auf Referenzgleichheit, kann letztendlich mit einem Hybrid konfrontiert werden: Einige Teile wurden frisch erstellt, andere wurden vor Ort verändert. Gleichzeitig wird jeder andere Codeabschnitt, der weiterhin eine Referenz auf den alten Zustand besitzt, diese Veränderung ebenfalls stillschweigend wahrnehmen.
Falls nur ein Teil eines verschachtelten Objekts tatsächlich geändert werden muss, sollte dieser Teil absichtlich kopiert werden, anstatt sich auf eine einzige Ebene der Ausbreitung zu verlassen.
const nextState = {
...state,
user: {
...state.user,
preferences: {
...state.user.preferences,
theme: "dark",
},
},
};
Es muss mehr eingegeben werden, aber jede veränderte Ebene erhält eine neue Identität, während die unberührten Teile weiterhin sicher geteilt werden können. Tools wie Immer können dieses Muster glätten, wenn Zustandsbäume tief und verschachtelt werden, doch sie beseitigen nicht die zugrunde liegende Anforderung: Man muss weiterhin wissen, welche Referenzen nach einem Update erhalten bleiben und welche nicht.
Die Spread-Syntax an sich ist fehlerfrei. Der Fehler entsteht durch die Annahme, dass eine flache Kopie eine tiefe Isolation bewirkt.
Jedes Mal, wenn Sie ein Objekt verbreiten, um es vor späteren Änderungen an anderer Stelle zu schützen, sollten Sie innehalten und überprüfen, welche verschachtelten Referenzen danach weiterhin geteilt werden. Diese Überprüfung ist weitaus wichtiger als die Frage, ob die Syntax zufällig ... verwendet.
4. Round-Tripping durch JSON.parse(JSON.stringify(...)) ist nicht dasselbe wie Klonen
Ein praktischer Trick, um Probleme mit oberflächlichen Kopien zu umgehen, ist dieser einzeilige Code:
const copy = JSON.parse(JSON.stringify(original));
Er scheint das Problem der verschachtelten Referenzen zu lösen, da die Verarbeitung der JSON-Stringe tatsächlich völlig neue Objekte erzeugt. Bei Daten, die bereits im Format von reinem JSON vorliegen, kann er sogar genau das liefern, was man sich erhofft hat.
Das Problem besteht darin, dieses Ergebnis als allgemeine Garantie für eine tiefe Klonierung zu betrachten.
JSON versteht nur ein begrenztes Datenmodell. Sobald Ihr Objekt Werte enthält, die außerhalb dieses Modells liegen, kann die Serialisierung diese stillschweigend ändern, entfernen oder Fehler auslösen.
Nehmen Sie dieses Objekt als Beispiel:
const value = {
createdAt: new Date(),
missing: undefined,
settings: new Map([["theme", "dark"]]),
};
Nach einer JSON-Übertragung ist die Date-Instanz nun nur noch eine Zeichenkette und hat ihr gesamtes verhaltensspezifisches Verhalten für Date verloren. Die Eigenschaft undefined verschwindet völlig. Der Map bleibt nicht als Map mit intakten Schlüssel-Wert-Paaren erhalten, was ein Problem darstellt, wenn der Aufrufer eine ordnungsgemäße strukturelle Kopie erwartet hat.
Zyklische Referenzen verschlimmern die Situation noch direkter:
const user = {
name: "Umar",
};
user.self = user;
JSON.stringify(user);
// TypeError
Andere Werttypen bringen ihre eigenen Probleme mit sich. BigInt-Werte können ohne zusätzliche Verarbeitung nicht mit dem normalen JSON.stringify übertragen werden, Eigenschaften mit Symbolen als Schlüsseln werden einfach weggelassen, und Instanzen von benutzerdefinierten Klassen verlieren das Prototyp-Objekt, das ihnen ursprünglich ihr klassenspezifisches Verhalten verliehen hat.
Nichts davon macht die JSON-Serialisierung unzuverlässig – sie erfüllt genau das, wofür Serialisierung da ist, nämlich JavaScript-Werte in die engere Form zu komprimieren, die JSON unterstützt.
Wenn man tatsächlich ein strukturelles Klonen benötigt, ist structuredClone() oft die bessere eingebaute Option, sofern die mitgearbeiteten Typen davon unterstützt werden. Für Daten, die eng mit dem eigenen Anwendungsbereich verbunden sind, kann das Schreiben einer expliziten Kopierfunktion noch zuverlässiger sein, da dabei genau festgelegt wird, welche Eigenschaften beibehalten werden sollen, anstatt davon auszugehen, dass alle Eigenschaften generell geklont werden sollten.
Auch ist zu beachten, dass tiefes Klonen oft völlig das falsche Werkzeug ist. Wenn man nur einen verschachtelten Teil eines Objekts ändern muss, verschwendet das Klonen der gesamten Struktur Zeit und erschwert es, zu erkennen, welcher Teil tatsächlich geändert wurde.
Eine gute Gewohnheit ist es, die Operation nach ihrem eigentlichen Zweck zu benennen. Wenn Sie etwas serialisieren möchten, tun Sie das absichtlich. Wenn Sie etwas klonen möchten, wählen Sie einen Klonierungsansatz, dessenes Verhalten tatsächlich den von Ihnen gespeicherten Werten entspricht.
5. Promise.all() begrenzt nicht, wie viele Operationen gleichzeitig ausgeführt werden
Stellen Sie sich eine Anwendung vor, die Details für eine Gruppe von Benutzern abrufen muss.
const users = await Promise.all(
userIds.map((id) => fetchUser(id))
);
Für eine kleine Anzahl von Benutzern funktioniert das gut und ist in der Regel die richtige Vorgehensweise. Der Code startet alle asynchronen Aufrufe, wartet gemeinsam auf deren Abschluss und gibt die Ergebnisse in derselben Reihenfolge zurück wie der Eingabearray.
Später wird dieselbe Funktion für ein Datensatz mit 50.000 Einträgen wiederverwendet.
const users = await Promise.all(
userIds.map((id) => fetchUser(id))
);
In der Syntax hat sich nichts geändert, doch die Auswirkungen haben es getan. Da .map() für jedes einzelne Element fetchUser aufruft und jeder Aufruf vermutlich sofort eine Netzwerk- oder Datenbankanfrage sendet, setzt Promise.all() keine Obergrenze dafür fest, wie viele dieser Anfragen gleichzeitig ausgeführt werden können.
Das kann einen nachgelagerten Dienst überlasten, einen Verbindungspool erschöpfen, Rate Limits überschreiten, den Speicherverbrauch in die Höhe treiben oder bei anstehenden Fehlern eine Welle von Neuanfragen auslösen. Visuell liest sich der Code weiterhin wie eine ordentliche Batch-Operation, wodurch leicht übersehen werden kann, was seine tatsächlichen Kosten sind.
In Promise.all() ist eine weitere versteckte Annahme enthalten: Sobald eine Promise abgelehnt wird, wird das Ganze abgelehnt. Das ist das richtige Verhalten, wenn man die Gruppe als eine einzige Arbeitseinheit betrachtet, bedeutet aber nicht, dass die anderen laufenden Operationen storniert werden – sie laufen weiter, selbst nachdem ein Fehler aufgetreten ist.
Wenn die Aufgaben tatsächlich unabhängig sind und man alle Ergebnisse unabhängig von einzelnen Fehlern haben möchte, eignet sich Promise.allSettled() oft besser für das, was man ausdrücken möchte.
const results = await Promise.allSettled(
userIds.map((id) => fetchUser(id))
);
Aber wenn Ihre eigentliche Sorge die Belastung eines anderen Systems ist, wird das Wechseln zwischen Promise.all() und Promise.allSettled() allein nichts daran ändern – keines dieser Verfahren begrenzt die Parallelität. Was Sie tatsächlich benötigen, ist Batching, ein Semaphor, eine Warteschlange oder ein Werkzeug zur Begrenzung der Parallelität, das nur eine feste Anzahl von Operationen gleichzeitig aktiv hält.
Dieses Muster funktioniert einwandfrei, solange die Eingangsdatenmenge klein und vorhersehbar bleibt und das Zielsystem die parallele Last bewältigen kann. Das Problem tritt später auf, wenn Logik, die ursprünglich für zehn Datensätze entwickelt wurde, auf zehntausend angewandt wird und niemand innehält, um zu prüfen, ob das immer noch sicher ist.
Parallelität hängt nicht davon ab, wie prägnant Sie auf Ihre Promises warten – sie hängt davon ab, wie viel Arbeit Sie starten und was die zugrunde liegenden Systeme tatsächlich bewältigen können.
6. Funktional aussehende Array-Methoden können dennoch mutieren
Array-Methoden in JavaScript ermöglichen kompakte, lesbare Datenpipelines – und das ist meistens tatsächlich ein Vorteil.
const activeUsers = users
.filter((user) => user.active)
.map((user) => user.name);
Weil sowohl filter() als auch map() neue Arrays zurückgeben, ist es verlockend anzunehmen, dass jede Kette von Array-Methoden das ursprüngliche Array unverändert lässt. Diese Annahme zerfällt jedoch sofort, sobald sort(), reverse(), splice() oder einige andere mutierende Methoden zur Kette hinzukommen.
Stellen Sie sich eine React-Komponente vor, die ein users-Array als Prop erhält:
function UserList({ users }) {
const sortedUsers = users.sort(
(a, b) => a.name.localeCompare(b.name)
);
return sortedUsers.map((user) => (
<User key={user.id} user={user} />
));
}
Es scheint, als würde dieser Code ein neues sortedUsers-Array erzeugen, doch sort() ordnet tatsächlich das ursprüngliche Array vor Ort um und gibt lediglich einen Verweis auf dasselbe Array zurück. Somit verändert ein Komponente, deren einzige Aufgabe das Rendering ist, als Nebeneffekt Daten, die zu ihrem Elternteil gehören.
Eine solche Mutation kann lange Zeit unentdeckt bleiben, bis ein anderer Teil der Anwendung von der ursprünglichen Reihenfolge abhängt. Die Tests für diese Komponente bestehen weiterhin, weil die angezeigte Ausgabe korrekt aussieht, während eine Schwesterkomponente oder ein anderswo memorisierter Wert stillschweigend mit umgeordneten Daten arbeitet, mit denen er nicht gerechnet hat.
Eine Lösung besteht darin, das Array vor Anwendung einer mutierenden Methode zu klonen.
const sortedUsers = [...users].sort(
(a, b) => a.name.localeCompare(b.name)
);
Neuere JavaScript-Runtimes bieten außerdem nicht-mutierende Äquivalente wie toSorted(), sofern die Zielumgebung sie unterstützt.
Die eigentliche Lektion hier besteht nicht darin, sich einzuprägen, welche Array-Methoden „sicher“ sind. Vielmehr geht es darum zu überlegen, ob eine Operation respektiert, wem die Daten gehören.
Falls einer Funktion ein Wert übergeben wird, den sie nicht besitzt, führt eine stille Mutation zu Verhaltensweisen, die nichts mit der ursprünglichen Aufgabe der Funktion zu tun haben. Der Code ist weitaus leichter nachvollziehbar, wenn entweder überhaupt keine Mutation stattfindet oder diese ausdrücklich an dem Ort erfolgt, der tatsächlich über den Wert verfügt.
7. Suchtabellen können ungültige Eingaben verbergen anstatt sie abzulehnen
Es ist üblich, einen switch-Satz durch ein einfaches Objekt als Dispatch-Tabelle zu ersetzen, da dies deklarativer erscheint.
const handlers = {
create: createUser,
update: updateUser,
delete: deleteUser,
};
handlers[action]?.(payload);
Das ist kompakt, und Dispatch per Suche ist in vielen Fällen tatsächlich ein gutes Muster. Was leicht übersehen wird, ist das, was passiert, wenn action nicht mit einer bekannten Schlüsselwert-Kombination übereinstimmt.
Weil dieses Beispiel auf optional chaining setzt, bewirkt eine nicht erkannte Aktion einfach nichts – still und leise.
action = "udpate"; // typo
handlers[action]?.(payload); // no handler runs
Ein einfacher Tippfehler kann nun als erfolgreiche, wirkungslose Aktion durch das System fließen. Sofern der Aufrufer keinen sinnvollen Rückgabewert überprüft, kann der Rest der Anwendung melden, dass die Operation erfolgreich war, obwohl tatsächlich nichts ausgeführt wurde.
Eine deutliche Fehlermeldung macht den Vertrag viel ehrlicher.
const handler = handlers[action];
if (!handler) {
throw new Error(`Unsupported action: ${action}`);
}
return handler(payload);
Suchtabellen werden riskanter, sobald die Schlüssel aus Benutzereingaben oder ungenügend überprüften Quellen stammen. Einfache Objekte erben außerdem Eigenschaften von ihrem Prototyp, es sei denn, man verhindert das absichtlich – daher erfordert die Behandlung willkürlicher Zeichenketten als sichere Eigenschaftssuche mehr Vorsicht als bei einer kleinen, intern bekannten Menge an Befehlen.
Durch die Verwendung eines Map-Objekts wird der wörterbuchartige Zweck klarer:
const handlers = new Map([
["create", createUser],
["update", updateUser],
["delete", deleteUser],
]);
const handler = handlers.get(action);
if (!handler) {
throw new Error(`Unsupported action: ${action}`);
}
return handler(payload);
In TypeScript kann die Kombination einer diskriminierten Union mit einem ausführlichen switch noch vorteilhafter sein, da das Hinzufügen einer neuen Aktion den Compiler dazu zwingt, jeden Ort im Codebase zu markieren, der aktualisiert werden muss.
Das bedeutet jedoch keineswegs, dass switch kategorisch besser ist als eine Abfrage-Tabelle. Dispatch-Tabellen sind dann nützlich, wenn Operationen naturgemäß datengetrieben sind und das Verhalten für unbekannte Schlüssel absichtlich festgelegt wurde.
Probleme entstehen, wenn Indirektheit es unmöglich macht, zwischen „Das Programm hat dies absichtlich verarbeitet“ und „Es ist überhaupt nichts passiert“ zu unterscheiden.
Die wahre Kosten der Cleverness sind versteckte Annahmen
Absolutes Verbot verdient keines dieser sieben Gewohnheiten.
Truthy-Prüfungen funktionieren gut, wenn die Eigenschaft „Wahrheit“ tatsächlich mit den Regeln des jeweiligen Anwendungsbereichs übereinstimmt. Optional Chaining eignet sich hervorragend für Beziehungen, die wirklich optional sind. Object Spread ist ideal für oberflächliche Aktualisierungen. JSON ist das natürliche Format für daten, die in JSON-Format vorliegen. Promise.all() ist perfekt geeignet für begrenzte parallele Aufgaben. Array-Methoden zur Änderung sind in Ordnung, wenn man das Array vollständig kontrolliert. Und Abfrachtabellen können die Logik zur Aufgabenverteilung wunderbar vereinfachen.
Das Risiko besteht darin, ihre bequemen Standardwerte als Garantien zu betrachten, die die Syntax tatsächlich nie bietet. !value bedeutet nicht „fehlend“. ?. bedeutet nicht „sicher vorhanden und gültig“. { ...obj } bedeutet nicht „vollständig unabhängige Kopie“. Die Serialisierung in JSON und wieder zurück bedeutet nicht „allgemeiner Klon“. Und Promise.all() bedeutet nicht „kontrollierte Konkurrenz“.
Deshalb ist die am besten wartbare Version eines JavaScript-Codeabschnitts nicht immer die kürzeste. Ein paar zusätzliche Zeilen, die undefined von 0 unterscheiden, eine fehlerhafte Objektstruktur ablehnen, parallele Anfragen begrenzen oder bei einer unerkannten Aktion einen Fehler auslösen, können Informationen bewahren, die zukünftige Wartungsteams sonst aus der Commit-Geschichte herausfinden müssten.
Wenn Ihnen ein Codeabschnitt besonders clever erscheint, nehmen Sie sich einen Moment Zeit, um zu prüfen, was dabei weggelassen wurde. Manchmal liegt die Erklärung darin, dass nur überflüssige Elemente entfernt wurden, während die Bedeutung erhalten blieb – genau das soll eine gute Abstraktion erreichen. In anderen Fällen wurde eine Unterscheidung weggelassen, die die aktuellen Anforderungen noch nicht erfordern.
Genau dieser zweite Fall neigt dazu, morgen die Fehler zu verursachen.
Eine nützliche Gewohnheit bei Code-Reviews ist es, zu prüfen, unter welchen Bedingungen ein kompakter Code weiterhin funktioniert, wenn sich die Anforderungen ändern. Wenn diese Bedingung wichtig ist, aber im Code nirgends sichtbar ist, schreiben Sie sie explizit auf, solange die Logik noch frisch und leicht verständlich ist.
Zukünftige Wartungsteams geraten selten in Schwierigkeiten, weil JavaScript es ihnen ermöglicht, zu viel Code zu schreiben. Sie geraten in Schwierigkeiten, weil der Code ihnen nicht mehr deutlich macht, welche Unterschiede tatsächlich von Bedeutung sind.