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.

Carpentry Tools
Photo von Étienne Girardet auf Unsplash

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: Wenn true, werden die Eigenschaften des Objekts entsprechend ihres Typs stilisiert. Standardmäßig true.

  • depth: Eine Zahl, die die Anzahl der Verschachtelungsebenen repräsentiert, die gedruckt werden, wenn ein Objekt andere Objekte oder Arrays enthält. Der Wert null bedeutet: alle Ebenen drucken. Standardmäßig 2.

  • showHidden: Ein boolescher Wert: Wenn true, werden die nicht aufzählbaren und Symbol-Eigenschaften des Objekts angezeigt. Standardmäßig false.

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

JSON.stringify() missbrauchen

Tücken von JavaScript `for...in`-Schleifen

Elektronische Rechnungen mit freier und quelloffener Software erzeugen

Dynamische Angular-Konfiguration

ImageMagick für Perl kompilieren

Angular Tour of heroes als Standalone-App

Diese Website verwendet Cookies und ähnliche Technologien, um gewisse Funktionalität zu ermöglichen, die Benutzbarkeit zu erhöhen und Inhalt entsprechend ihren Interessen zu liefern. Über die technisch notwendigen Cookies hinaus können abhängig von ihrem Zweck Analyse- und Marketing-Cookies zum Einsatz kommen. Sie können ihre Zustimmung zu den vorher erwähnten Cookies erklären, indem sie auf "Zustimmen und weiter" klicken. Hier können sie Detaileinstellungen vornehmen oder ihre Zustimmung - auch teilweise - mit Wirkung für die Zukunft zurücknehmen. Für weitere Informationen lesen sie bitte unsere Datenschutzerklärung.