Uncaught TypeError: Cannot read property 'foo' of undefined. Der gefürchtete Fehler, auf den wir alle irgendwann in der JavaScript-Entwicklung stoßen. Es könnte ein leerer Zustand von einer API sein, die anders zurückgibt, als Sie erwartet haben. Es könnte etwas anderes sein. Wir wissen es nicht, weil der Fehler selbst so allgemein und breit ist.
Ich hatte kürzlich ein Problem, bei dem bestimmte Umgebungsvariablen aus dem einen oder anderen Grund nicht geladen wurden, was zu allerlei Komplikationen führte, während dieser Fehler direkt vor mir stand. Was auch immer die Ursache war, es kann ein katastrophaler Fehler sein, wenn er unberücksichtigt bleibt. Wie können wir ihn also von vornherein verhindern?
Lassen Sie es uns herausfinden.
Hilfsbibliothek
Wenn Sie bereits eine Hilfsbibliothek in Ihrem Projekt verwenden, besteht eine gute Chance, dass diese eine Funktion zur Vermeidung dieses Fehlers enthält. _.get in Lodash (Doku) oder R.path in Ramda (Doku) ermöglichen den sicheren Zugriff auf das Objekt.
Wenn Sie bereits eine Hilfsbibliothek verwenden, ist dies wahrscheinlich die einfachste Lösung. Wenn Sie keine Hilfsbibliothek verwenden, lesen Sie weiter!
Kurzschluss mit &&
Eine interessante Tatsache über logische Operatoren in JavaScript ist, dass sie nicht immer einen booleschen Wert zurückgeben. Laut Spezifikation ist „der von einem && oder || Operator erzeugte Wert nicht notwendigerweise vom Typ Boolean. Der erzeugte Wert ist immer der Wert eines der beiden Operanden-Ausdrücke.“
Im Fall des && Operators wird der erste Ausdruck verwendet, wenn er ein „falsy“ Wert ist. Andernfalls wird der zweite Ausdruck verwendet. Das bedeutet, dass der Ausdruck 0 && 1 als 0 (ein falsy Wert) ausgewertet wird und der Ausdruck 2 && 3 als 3 ausgewertet wird. Wenn mehrere && Ausdrücke verkettet sind, werden sie entweder zum ersten falsy Wert oder zum letzten Wert ausgewertet. Zum Beispiel wird 1 && 2 && 3 && null && 4 als null ausgewertet, und 1 && 2 && 3 wird als 3 ausgewertet.
Wie ist das nützlich für den sicheren Zugriff auf verschachtelte Objekteigenschaften? Logische Operatoren in JavaScript „kurzschließen“. Im Fall von && bedeutet dies, dass der Ausdruck nach Erreichen des ersten falsy Wertes nicht weiter fortgesetzt wird.
const foo = false && destroyAllHumans();
console.log(foo); // false, and humanity is safe
In diesem Beispiel wird destroyAllHumans niemals aufgerufen, da der && Operand die gesamte Auswertung nach false gestoppt hat.
Dies kann verwendet werden, um sicher auf verschachtelte Eigenschaften zuzugreifen.
const meals = {
breakfast: null, // I skipped the most important meal of the day! :(
lunch: {
protein: 'Chicken',
greens: 'Spinach',
},
dinner: {
protein: 'Soy',
greens: 'Kale',
},
};
const breakfastProtein = meals.breakfast && meals.breakfast.protein; // null
const lunchProtein = meals.lunch && meals.lunch.protein; // 'Chicken'
Neben seiner Einfachheit ist einer der Hauptvorteile dieses Ansatzes seine Kürze bei der Handhabung kleiner Ketten. Beim Zugriff auf tiefere Objekte kann dies jedoch ziemlich wortreich sein.
const favorites = {
video: {
movies: ['Casablanca', 'Citizen Kane', 'Gone With The Wind'],
shows: ['The Simpsons', 'Arrested Development'],
vlogs: null,
},
audio: {
podcasts: ['Shop Talk Show', 'CodePen Radio'],
audiobooks: null,
},
reading: null, // Just kidding -- I love to read
};
const favoriteMovie = favorites.video && favorites.video.movies && favorites.video.movies[0];
// Casablanca
const favoriteVlog = favorites.video && favorites.video.vlogs && favorites.video.vlogs[0];
// null
Je tiefer ein Objekt verschachtelt ist, desto unhandlicher wird es.
Das „Maybe Monad“
Oliver Steele hat sich diese Methode ausgedacht und sie in seinem Blogbeitrag „Monads on the Cheap I: The Maybe Monad.“ ausführlicher behandelt. Ich werde versuchen, hier eine kurze Erklärung zu geben.
const favoriteBook = ((favorites.reading||{}).books||[])[0]; // undefined
const favoriteAudiobook = ((favorites.audio||{}).audiobooks||[])[0]; // undefined
const favoritePodcast = ((favorites.audio||{}).podcasts||[])[0]; // 'Shop Talk Show'
Ähnlich wie im obigen Kurzschlussbeispiel prüft diese Methode, ob ein Wert falsy ist. Wenn ja, wird versucht, die nächste Eigenschaft eines leeren Objekts abzurufen. Im obigen Beispiel ist favorites.reading null, daher wird die books Eigenschaft von einem leeren Objekt abgerufen. Dies führt zu einem undefined, und so wird 0 ebenfalls von einem leeren Array abgerufen.
Der Vorteil dieser Methode gegenüber der && Methode ist, dass sie die Wiederholung von Eigenschaftsnamen vermeidet. Bei tieferen Objekten kann dies ein erheblicher Vorteil sein. Der Hauptnachteil wäre die Lesbarkeit – es ist kein gängiges Muster und erfordert vom Leser möglicherweise einen Moment, um zu verstehen, wie es funktioniert.
try/catch
try...catch Anweisungen in JavaScript bieten eine weitere Methode zum sicheren Zugriff auf Eigenschaften.
try {
console.log(favorites.reading.magazines[0]);
} catch (error) {
console.log("No magazines have been favorited.");
}
Leider sind try...catch Anweisungen in JavaScript keine Ausdrücke. Sie werten keinen Wert aus, wie sie es in einigen Sprachen tun. Dies verhindert eine prägnante try Anweisung als Möglichkeit, eine Variable zu setzen.
Eine Option ist die Verwendung einer let Variable, die im Block über der try...catch definiert ist.
let favoriteMagazine;
try {
favoriteMagazine = favorites.reading.magazines[0];
} catch (error) {
favoriteMagazine = null; /* any default can be used */
};
Obwohl es wortreich ist, funktioniert dies zum Setzen einer einzelnen Variablen (d. h. wenn die veränderliche Variable Sie nicht abschreckt). Probleme können jedoch auftreten, wenn sie in großer Menge durchgeführt werden.
let favoriteMagazine, favoriteMovie, favoriteShow;
try {
favoriteMovie = favorites.video.movies[0];
favoriteShow = favorites.video.shows[0];
favoriteMagazine = favorites.reading.magazines[0];
} catch (error) {
favoriteMagazine = null;
favoriteMovie = null;
favoriteShow = null;
};
console.log(favoriteMovie); // null
console.log(favoriteShow); // null
console.log(favoriteMagazine); // null
Wenn einer der Zugriffsversuche auf die Eigenschaft fehlschlägt, fallen alle auf ihre Standardwerte zurück.
Eine Alternative ist, die try...catch in eine wiederverwendbare Hilfsfunktion zu verpacken.
const tryFn = (fn, fallback = null) => {
try {
return fn();
} catch (error) {
return fallback;
}
}
const favoriteBook = tryFn(() => favorites.reading.book[0]); // null
const favoriteMovie = tryFn(() => favorites.video.movies[0]); // "Casablanca"
Durch das Verpacken des Zugriffs auf das Objekt in einer Funktion können Sie den „unsicheren“ Code verzögern und ihn in eine try...catch übergeben.
Ein Hauptvorteil dieser Methode ist, wie natürlich der Zugriff auf die Eigenschaft ist. Solange Eigenschaften in eine Funktion verpackt sind, werden sie sicher abgerufen. Ein Standardwert kann auch im Falle eines nicht existierenden Pfades angegeben werden.
Zusammenführen mit einem Standardobjekt
Durch das Zusammenführen eines Objekts mit einem ähnlich geformten Objekt mit „Standards“ können wir sicherstellen, dass der Pfad, den wir abzurufen versuchen, sicher ist.
const defaults = {
position: "static",
background: "transparent",
border: "none",
};
const settings = {
border: "1px solid blue",
};
const merged = { ...defaults, ...settings };
console.log(merged);
/*
{
position: "static",
background: "transparent",
border: "1px solid blue"
}
*/
Seien Sie jedoch vorsichtig, da das gesamte verschachtelte Objekt überschrieben werden kann, anstatt einer einzelnen Eigenschaft.
const defaults = {
font: {
family: "Helvetica",
size: "12px",
style: "normal",
},
color: "black",
};
const settings = {
font: {
size: "16px",
}
};
const merged = {
...defaults,
...settings,
};
console.log(merged.font.size); // "16px"
console.log(merged.font.style); // undefined
Oh nein! Um dies zu beheben, müssen wir ähnlich jedes der verschachtelten Objekte kopieren.
const merged = {
...defaults,
...settings,
font: {
...defaults.font,
...settings.font,
},
};
console.log(merged.font.size); // "16px"
console.log(merged.font.style); // "normal"
Viel besser!
Dieses Muster ist üblich bei Plugins oder Komponenten, die ein großes Einstellungs-Objekt mit integrierten Standardwerten akzeptieren.
Ein Bonus dieser Vorgehensweise ist, dass wir durch das Schreiben eines Standardobjekts eine Dokumentation darüber erstellen, wie ein Objekt aussehen sollte. Leider kann je nach Größe und Struktur der Daten das „Zusammenführen“ durch das Kopieren jedes verschachtelten Objekts unübersichtlich werden.
Die Zukunft: Optional Chaining
Derzeit gibt es einen TC39-Vorschlag für eine Funktion namens „Optional Chaining“. Dieser neue Operator würde so aussehen
console.log(favorites?.video?.shows[0]); // 'The Simpsons'
console.log(favorites?.audio?.audiobooks[0]); // undefined
Der ?. Operator funktioniert durch Kurzschluss: Wenn die linke Seite des ?. Operators zu null oder undefined ausgewertet wird, wird der gesamte Ausdruck zu undefined ausgewertet und die rechte Seite bleibt unerwogen.
Um einen benutzerdefinierten Standardwert zu haben, können wir den || Operator im Falle eines undefined verwenden.
console.log(favorites?.audio?.audiobooks[0] || "The Hobbit");
Welche Methode sollten Sie verwenden?
Die Antwort, wie Sie vielleicht erraten haben, ist die alte Antwort… „es kommt darauf an“. Wenn der Optional Chaining Operator zur Sprache hinzugefügt wurde und die erforderliche Browserunterstützung vorhanden ist, ist dies wahrscheinlich die beste Option. Wenn Sie jedoch nicht aus der Zukunft stammen, gibt es mehr zu berücksichtigen. Verwenden Sie eine Hilfsbibliothek? Wie tief ist Ihr Objekt verschachtelt? Müssen Sie Standardwerte angeben? Unterschiedliche Fälle können einen anderen Ansatz erfordern.
Es gibt auch https://github.com/burakcan/mb
Welches ist ein winziges (1 Zeile) Hilfsprogramm, das Ihnen hilft, Accessoren für verschachtelte Eigenschaften zu erstellen, die sicher undefined zurückgeben, anstatt abzustürzen
Wow. Danke dafür.
Ich verwende für so etwas viel Destrukturierung.
Ich wollte gerade dasselbe sagen.
Meine Lösung ist die Verwendung von striktem TypeScript und die Überwachung mit Sentry.
Ich habe gerade nach diesem Thema gefragt und von https://github.com/facebookincubator/idx erfahren, das dieses Problem ebenfalls löst. Tatsächlich frage ich mich, ob man es nicht komplett über eine Art Babel-Transformation abstrahieren könnte… Optional Chaining klingt jedoch nach der besten Lösung!
Ich habe früher den „Optional Chaining“-Operator in Groovy verwendet – eine meiner Lieblingsfunktionen!
Schöner Artikel.
Ich habe kürzlich darüber nachgedacht, über genau dieses Thema zu bloggen, nachdem mein Kollege und ich darüber gesprochen hatten. Wir hatten Kurzschlüsse satt. Ich hoffe wirklich, dass der Optional Chaining Operator bald Realität wird. Wussten Sie, dass er auch Elvis Operator genannt wird, weil er ein bisschen wie sein Haar aussieht?
Ich finde, dass das Maybe Monad ziemlich unordentlich ist und die Standardwerte schwer zu verwalten wären (in unserem Anwendungsfall). Ich werde mich vorerst an das Lodash _.get Hilfsprogramm halten.
Vielleicht, wenn ich auf der Insel bin, werde ich eine Hilfsfunktion wie diese verwenden.
function get(exp, defaultvalue){
try{ return eval(exp)}
catch{
return defaultvalue
}
}
Ich denke, Lodash hat mit _.get() eine wirklich schöne Option.