JSON.stringify() missbrauchen
Es ist ziemlich beliebt, JSON.stringify
sachfremd zu missbrauchen, aber dabei wird oft übersehen, dass dies einige Überraschungen bergen kann. Dieser Blog-Post beschäftigt sich damit, wie diese Probleme umschifft werden können, indem man einfach die richtigen Werkzeuge verwendet.
Use Case 1: Pretty-Printing von Datenstrukturen
Front-End-Entwicklerinnen und -Entwickler sind oft frustriert, wenn sie herausfrinden, dass console.log()
außerhalb des Browsers nicht nur nicht-interaktiv ist, sondern auch nur die ersten drei Ebenen tief verschachtelter Objekte ausgibt. Tiefer verschachtelte Objekte werden nur noch anonym als [Object]
oder [Array]
dargestellt.
Als Allheilmittel dagegen wird of console.log(JSON.stringify(obj, null, '\t'))
empfohlen. Die Extra-Argumente null
und '\t'
(oder eine Zahl für die Einrückungstiefe) sorgen dabei dafür, dass JSON.stringify
eine lesbare Form des Objekts generiert.
Use Case 2: Deep-Cloning
Mit JSON.parse(JSON.stringify(obj))
lässt sich auch ein Deep-Clone von Objekten erzeugen. Nicht sehr elegant, nicht sehr effizient, aber dennoch sehr verbreitet.
Probleme beim Verwenden des JSON
-Namespace-Objekts
JSON wurde erfunden, um Daten, zwischen in beliebigen Programmiersprachen geschriebenen Anwendungen auszutauschen. Die Tatsache, dass JSON für JavaScript Object Notation steht, bedeutet nicht, dass JSON jeden Detailaspekt eines JavaScript-Objekts beschreiben kann. Bei der Serialisierung in JSON werden viele Informationen modifiziert, und einige sogar schlichtweg verworfen.
Stillschweigende Modifikationen
Einige Dinge wie zyklische Referenzen funktioniert mit JSON einfach nicht. Das sollte jedem klar sein. Aber jetzt zu den weniger klaren Fällen.
Wo sind meine undefinierten Werte?
Als ich einmal an einer Applikation schrieb, wo die Unterscheidung zwischen undefined
und null
signifikant war, konnte ich sehen, dass an allen Stellen, an denen ich ein undefined
erwartete, der Wert null
stand. Nach langen und fruchtlosen Debugging-Sessions ging mir plötzlich auf, dass mein eigener Debugging-Code daran schuld war. Ich verwendete console.log(JSON.stringify(obj, null, '\t'))
und das wandelte natürlich jeden nicht definierten Wert in ein null
um. Das ist nichts, dass man leichten Herzens den anderen Leuten auf der Arbeit erzählt.
NaN
und Infinity
Diese Werte werden ebenfalls zu null
konvertiert.
Besonders perfide ist hierbei, dass bei den Unendlichkeitswerten das Vorzeichen wegfällt:
const original = { big: +Infinity, medium: 0, small: -Infinity };
console.log(original.big > original.medium); // true
console.log(original.medium > original.small); // true
const clone = JSON.parse(JSON.stringify(original));
console.log(clone.big > clone.medium); // false
console.log(clone.medium > clone.small); // false
console.log(clone);
Das sollte zwar für niemanden eine Überraschung darstellen, ist aber einfach zu vergessen, wenn man im Kopf die Abkürzung JSON.parse(JSON.stringify(obj))
ist gleich "Deep-Cloning" genommen hat.
Funktionen
Probieren wir das hier:
const obj = {
foo: () => {},
};
console.log(JSON.stringify(obj, null, '\t'));
Die Ausgabe ist ein leeres Objekt {}
. Eine Funktion kann nicht in JSON kodiert werden und wird daher verworfen. Überraschend kann dabei aber sein, dass nicht nur der Wert, sondern die ganze Eigenschaft verworfen wird. Der Schlüssel foo
existiert nach der Umwandlung nicht mehr!
Non-Enumerable- und Symbol-Properties
Ein weiteres Beispiel:
const obj = { [Symbol('key')]: 'value' };
Object.defineProperty(obj, 'hidden', { value: 'secret', enumerable: false });
console.log(JSON.stringify(obj, null, '\t'));
Das konnten wir uns schon denken. Die Ausgabe ist einfach {}
.
Explizite Serialisierung mit toJSON()
Beispiel:
let obj = {
value: 42,
toJSON() {
return { onlyValue: this.value };
}
};
console.log(JSON.stringify(obj)); // {"onlyValue":42}
Die Anwesenheit einer Methode toJSON()
fällt nur auf, wenn in JSON serialisiert wird. Und das kann eine Überraschung sein.
Prototypen werden ignoriert
Geerbte Eigenschaften eines Prototyps werden nicht serialisiert; nur die eigenen Eigenschaften werden übernommen:
function Person(name) {
this.name = name;
}
Person.prototype.age = 30;
const person = new Person('Alice');
console.log(JSON.stringify(person)); // {"name":"Alice"}
Puh!
Nicht-numerische Array-Indizes
Kürzlich haben wir uns Array-Indizes im Post Tücken von JavaScript for...in
-Schleifen etwas genauer angeschaut, und dabei festgestellt, dass man Arrays in JavaScript auch mit beliebigen Strings indizieren kann.
const arr = ['foo', 'bar', 'baz'];
arr['cowboys'] = ['Adam', 'Hoss', 'Little Joe'];
console.log(JSON.stringify(arr)); // ["foo","bar","baz"]
Where have all my cowboys gone?
Nicht falsch verstehen. Ich versuche keine Mängel in der JSON-Definition zu finden. Es gibt einen guten Grund dafür, dass alle betrachteten Fälle genau so behandelt werden, wie sie behandelt werden. Wenn man allerdings JSON.stringify()
für etwas anderes als normalisierten Datenaustausch missbraucht, wird man früher oder später eine dieser speziellen Regeln vergessen, und garantiert im falschen Augenblick.
Wie es richtig geht
Zum Glück gibt es bessere Möglichkeiten, sowohl für die Anzeige tief verschachtelter Daten als auch für Deep Cloning.
Tief verschachtelte Daten anzeigen
Um tief verschachtelte Daten anzuzeigen, zum Beispiel zum Debuggen, kann man die statische Methode console.dir()
verwenden. Sie akzeptiert ein optionales zweites Argument options
, mit dem sich das Verhalten steuern lässt. Das Argument options
kann die folgenden optionalen Eigenschaften haben:
color
: Ein boolescher Wert: Wenntrue
, werden die Eigenschaften des Objekts entsprechend ihres Typs stilisiert. Standardmäßigtrue
.depth
: Eine Zahl, die die Anzahl der Verschachtelungsebenen repräsentiert, die gedruckt werden, wenn ein Objekt andere Objekte oder Arrays enthält. Der Wertnull
bedeutet: alle Ebenen drucken. Standardmäßig 2.showHidden
: Ein boolescher Wert: Wenntrue
, werden die nicht aufzählbaren und Symbol-Eigenschaften des Objekts angezeigt. Standardmäßigfalse
.
Wir ersetzen also einfach diesen Aufruf:
console.log(JSON.stringify(obj, null, 4));
Mit diesem Aufruf:
console.dir(obj, { depth: null });
Das ist ist sogar kürzer. In einigen esoterischen Fällen sollten wir noch die Option showHidden
verwenden, wenn nicht aufzählbare und Symbol-Eigenschaftenten ebenfalls ausgegeben werden sollen.
Deep-Cloning
Heutzutage sollte es fast immer möglich sein, structuredClone() zu verwenden:
const copy = structuredClone(original);
Die Methode structuredClone()
ist mittlerweile fast universell verfügbar, und in den übrigen Fällen kann stattdessen auf Lodashs cloneDeep() zurückgegriffen werden. Beide sind besser als JSON.parse(JSON.stringify(obj))
.
Zusammenfassung
Es ist nichts verkehrt an JSON.stringify()
, und es ist auch nichts verkehrt daran, die Methode zu verwenden oder zu missbrauchen. Verwendet man JSON.stringify()
allerdings für Dinge, für die die Methode nicht gedacht ist, läuft man Gefahr zu übersehen oder zu vergessen, dass sie sich in Spezialfällen überraschend verhält. Dieses Risiko lässt sich ganz einfach vermeiden, indem man stattdessen auf console.dir()
oder structuredClone()
zurückgreift.
Leave a comment