7 Sztuczek JSON, Które Oszczędzą Ci Godziny
Każdy programista JavaScript używa JSON.parse i JSON.stringify dziesiątki razy w tygodniu. Ale większość zatrzymuje się na podstawach - parsowanie odpowiedzi API i serializowanie obiektów do stringów. API ma więcej do zaoferowania, a niektóre z mniej używanych funkcji rozwiązują problemy, które inaczej wymagałyby bibliotek zewnętrznych lub godzin debugowania.
Ten przewodnik pokrywa siedem sztuczek wykraczających poza domyślne ustawienia. Każda jest natychmiast stosowalna do prawdziwych baz kodu. Bez abstrakcji, bez wymyślonych przykładów - to wzorce pojawiające się w systemach produkcyjnych.
Możesz zweryfikować i przejrzeć dowolny z przykładów kodu w tym artykule używając narzędzia Toova JSON Formatter, które waliduje i formatuje JSON całkowicie w twojej przeglądarce.
1. Deep Clone z Round-Tripem JSON (i Jego Ograniczenia)
Najstarsza sztuczka z arsenału JavaScript: użyj JSON.parse(JSON.stringify(obj)), aby stworzyć głęboką kopię zwykłego obiektu.
const original = { a: 1, b: { c: 2 } };
// Naiwne podejście - głęboko zagnieżdżone obiekty wciąż są współdzielone
const shallowCopy = { ...original }; // b nadal wskazuje na ten sam obiekt
// JSON deep clone - tworzy całkowicie niezależną kopię
const deepClone = JSON.parse(JSON.stringify(original));
deepClone.b.c = 99;
console.log(original.b.c); // 2 - oryginał nienaruszony Działa to, ponieważ serializowanie do stringa i parsowanie z powrotem tworzy całkowicie nowe drzewo obiektów bez współdzielonych referencji. Jest szybkie, nie wymaga zależności i jest dostępne od ES5.
Nowoczesna alternatywa dla użycia w pamięci to structuredClone():
// Nowoczesna alternatywa: structuredClone() - obsługuje więcej typów
// Wspierane w Node.js 17+ i wszystkich zawsze aktualnych przeglądarkach
const clone = structuredClone(original); Poznaj ograniczenia podejścia JSON, zanim na nim polegniesz:
// JSON.parse/stringify NIE potrafi obsłużyć:
const broken = {
date: new Date(), // staje się stringiem - traci prototyp Date
fn: () => 'hello', // po cichu pominięte
undef: undefined, // po cichu pominięte
inf: Infinity, // staje się null
map: new Map(), // staje się {}
cycle: null, // referencje cykliczne rzucają wyjątek
};
Jeśli twój obiekt zawiera tylko zwykłe dane - stringi, liczby, booleany, tablice i zagnieżdżone zwykłe obiekty - round-trip JSON jest bezpieczny i szybki. Dla wszystkiego innego preferuj structuredClone() lub dedykowaną bibliotekę.
2. Niestandardowy Replacer do Filtrowania i Maskowania
Większość programistów wie, że JSON.stringify przyjmuje drugi argument, ale rzadko go używa. Tym drugim argumentem jest replacer: albo tablica kluczy do uwzględnienia, albo funkcja kontrolująca dokładnie, jak każda wartość jest serializowana.
Replacer jako tablica - whitelistuj konkretne klucze:
const user = {
id: 'u_001',
name: 'Alice',
password: 'hunter2', // nie może pojawić się w logach
creditCard: '4111111111111111', // nie może pojawić się w logach
role: 'admin',
};
// Replacer jako tablica: uwzględnij tylko te klucze
JSON.stringify(user, ['id', 'name', 'role']);
// '{"id":"u_001","name":"Alice","role":"admin"}' Replacer jako funkcja - transformuj lub redaguj wartości:
// Replacer jako funkcja: pełna kontrola nad kluczem/wartością
const masked = JSON.stringify(user, (key, value) => {
if (key === 'password' || key === 'creditCard') return '[REDACTED]';
if (key === '' ) return value; // obiekt root - zawsze zwracaj
return value;
});
// '{"id":"u_001","name":"Alice","password":"[REDACTED]","creditCard":"[REDACTED]","role":"admin"}' Bardziej zaawansowana wersja maskuje wartości na podstawie ich kształtu, a nie nazwy klucza:
// Replacer do maskowania na podstawie typu
const sanitize = (key, value) => {
if (typeof value === 'string' && value.match(/^4[0-9]{15}$/)) {
return '****-****-****-' + value.slice(-4);
}
return value;
}; Ta technika jest niezbędna dla middleware logującego: chcesz ustrukturyzowane logi z pełnym kontekstem obiektu, ale niektóre pola nie mogą nigdy dotrzeć do agregatora logów. Replacer pozwala obsłużyć to na granicy serializacji, zamiast rozsiewać logikę redakcji po całej bazie kodu.
3. Deterministyczne Sortowanie Kluczy
Kolejność kluczy obiektu w JavaScript to kolejność wstawiania (dla kluczy stringowych). Dwa obiekty z tymi samymi kluczami, ale utworzone w różnej kolejności, produkują różne stringi JSON, co psuje naiwne sprawdzania równości, klucze cache i hashe zawartości.
function sortedStringify(obj) {
return JSON.stringify(obj, Object.keys(obj).sort());
}
const a = { z: 1, a: 2, m: 3 };
const b = { a: 2, m: 3, z: 1 };
sortedStringify(a) === sortedStringify(b); // true - kolejność kluczy znormalizowana Dla głęboko zagnieżdżonych obiektów zastosuj sortowanie rekurencyjnie:
// Rekurencyjne sortowanie kluczy dla zagnieżdżonych obiektów
function sortKeys(value) {
if (Array.isArray(value)) return value.map(sortKeys);
if (value !== null && typeof value === 'object') {
return Object.fromEntries(
Object.keys(value).sort().map((k) => [k, sortKeys(value[k])])
);
}
return value;
}
const sorted = JSON.stringify(sortKeys(deepNested)); Posortowany JSON jest niezbędny, gdy:
- Generujesz klucze cache z ciał żądań
- Obliczasz sumy kontrolne lub podpisy nad payloadami JSON
- Porównujesz odpowiedzi API w testach niezależnie od kolejności pól
- Przechowujesz obiekty konfiguracyjne, gdzie kolejność nie powinna wpływać na równość
Po sortowaniu i pretty-printingu, użyj narzędzia Text Diff, aby porównać dwa znormalizowane stringi JSON i zobaczyć dokładnie, które wartości zmieniły się między wersjami.
4. Obsługa BigInt Bez Utraty Precyzji
Typ Number w JavaScript może bezpiecznie reprezentować liczby całkowite do 253 − 1. Dla identyfikatorów generowanych przez systemy rozproszone, kwot finansowych w jednostkach mniejszych lub znaczników czasu w nanosekundach to za mało. BigInt pokrywa liczby całkowite o dowolnej precyzji, ale JSON.stringify nie wie, co z nimi zrobić.
const data = {
amount: 9007199254740993n, // większe niż Number.MAX_SAFE_INTEGER
};
// To rzuca: TypeError: Do not know how to serialize a BigInt
JSON.stringify(data); // BŁĄD Standardowe obejścia:
// Rozwiązanie 1: Konwersja do stringa za pomocą replacera
JSON.stringify(data, (key, value) =>
typeof value === 'bigint' ? value.toString() : value
);
// '{"amount":"9007199254740993"}'
// Rozwiązanie 2: toJSON() na prototypie BigInt (monkey-patch - używaj ostrożnie)
BigInt.prototype.toJSON = function () { return this.toString(); };
JSON.stringify(data); // '{"amount":"9007199254740993"}' Po stronie parsowania, funkcja reviver może przywrócić wartości BigInt z ich reprezentacji string:
// Reviver przywracający BigInt przy parsowaniu
const revived = JSON.parse('{"amount":"9007199254740993"}', (key, value) => {
if (key === 'amount') return BigInt(value);
return value;
});
console.log(typeof revived.amount); // 'bigint'
Trzymaj konwersję BigInt-na-string we wspólnej warstwie serializacji, aby była stosowana spójnie. Pozwalanie, by BigInty przeciekły do ad hoc wywołań JSON.stringify na krawędziach bazy kodu, prowadzi do nieprzewidywalnych błędów, które trudno wytropić.
5. Wykrywanie Referencji Cyklicznych
Referencja cykliczna występuje, gdy obiekt zawiera referencję do samego siebie lub do przodka w grafie obiektów. Są częstsze, niż mogłoby się wydawać: event emitery, węzły DOM, węzły React fiber i encje ORM często mają back-references.
// Przykład referencji cyklicznej
const obj = { name: 'node' };
obj.self = obj; // obj odnosi się do samego siebie
JSON.stringify(obj); // rzuca: TypeError: Converting circular structure to JSON Obsłuż to za pomocą niestandardowego replacera śledzącego odwiedzone obiekty:
// Ręczny handler referencji cyklicznej
function safeStringify(obj) {
const seen = new WeakSet();
return JSON.stringify(obj, (key, value) => {
if (typeof value === 'object' && value !== null) {
if (seen.has(value)) return '[Circular]';
seen.add(value);
}
return value;
});
}
safeStringify(obj); // '{"name":"node","self":"[Circular]"}' WeakSet to właściwa struktura danych: trzyma referencje do obiektów bez blokowania garbage collection, a wyszukiwanie jest O(1). Ten wzorzec działa również w middleware logującym, gdzie chcesz, by błędy degradowały się łagodnie, zamiast rzucać podczas serializacji.
Częsty wariant: zamiast oznaczać referencje cykliczne jako [Circular], zastąp je stringiem ścieżki typu [Circular: $.config.parent], aby uczynić lokalizację referencji jawną podczas debugowania.
6. Pretty-Print Tablic w Pojedynczej Linii
Trzeci argument JSON.stringify to wcięcie. Przekazanie 2 lub 4 rozszerza wszystko na wiele linii, co jest świetne dla obiektów, ale może być rozwlekłe dla tablic prymitywów, takich jak tagi, ID czy współrzędne.
const mixed = {
title: 'Report',
tags: ['json', 'api', 'debug'],
config: { indent: 2, sortKeys: true },
count: 42,
};
// Domyślnie: wszystko wieloliniowo
JSON.stringify(mixed, null, 2);
// {
// "title": "Report",
// "tags": [
// "json",
// "api",
// "debug"
// ],
// "config": {
// "indent": 2,
// "sortKeys": true
// },
// "count": 42
// } Możesz po-przetworzyć wyjście, aby zwinąć proste tablice w pojedynczą linię:
// Niestandardowy replacer dla tablic w jednej linii
function prettyMixed(obj) {
const raw = JSON.stringify(obj, null, 2);
// Zwiń tablice zawierające tylko prymitywy w jedną linię
return raw.replace(
/\[\n\s+([\s\S]*?)\n\s+\]/g,
(match, inner) => {
const items = inner.split(',\n').map((s) => s.trim());
if (items.every((s) => !/^[{\[]/.test(s))) {
return '[' + items.join(', ') + ']';
}
return match;
}
);
} Wynik: obiekty pozostają wieloliniowe dla czytelności, tablice prymitywów zwijają się do jednej linii dla zwartości. To format używany w wielu plikach konfiguracyjnych i wyjściach ustrukturyzowanych logów, gdzie ludzie i maszyny muszą czytać te same dane. JSON Formatter i konwerter JSON na YAML obsługują to zwijanie automatycznie.
7. Streaming Dużego JSON
Ładowanie dużego pliku JSON w całości do pamięci przed parsowaniem to najczęstszy błąd wydajnościowy w pipeline'ach przetwarzania JSON. Odpowiedź JSON o rozmiarze 100 MB alokuje co najmniej 100 MB sterty na surowy string, a następnie drugą alokację na sparsowany obiekt. Dla plików powyżej kilku megabajtów parser streamingowy przetwarza dane przyrostowo.
W Node.js z biblioteką streamingową:
// node --experimental-vm-modules (lub użyj biblioteki streamującej JSON)
// Natywne streamowanie z propozycją JSON Source Map (Stage 3, 2026):
// Na razie użyj biblioteki typu @streamparser/json lub jsonstream2
import { createReadStream } from 'fs';
import parser from '@streamparser/json';
const jsonParser = new parser.JSONParser();
jsonParser.onValue = ({ value, key, parent }) => {
if (key === 'id') {
console.log('Found id:', value);
}
};
createReadStream('large.json').pipe(jsonParser); W przeglądarce za pomocą Fetch Streams API:
// Streamowanie po stronie przeglądarki z Fetch + dekoder strumienia JSON:
const response = await fetch('/api/large-data');
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
// Przetwórz kompletne obiekty JSON z bufora...
}
Streaming jest najbardziej użyteczny w pipeline'ach przetwarzania wsadowego, dużych endpointach eksportu i skryptach analizy logów. Dla typowych odpowiedzi API poniżej 1 MB standardowy JSON.parse jest wystarczająco szybki i znacznie prostszy. Próg, od którego streaming zaczyna się opłacać w praktyce, to około 5-10 MB, w zależności od złożoności sparsowanej struktury i rozmiaru sterty docelowego środowiska.
Aby zbadać strukturę dużych odpowiedzi JSON przed zbudowaniem parsera streamingowego, wklej próbkę do JSON Formattera, aby zobaczyć, które ścieżki zawierają potrzebne dane, a następnie konwertuj do CSV lub YAML do dalszej analizy.
Wszystko Razem
Te siedem technik pokrywa najbardziej wpływowe zakątki API JSON:
- Deep clone via round-trip - szybki dla zwykłych danych, znaj ograniczenia
- Replacer - filtruj klucze i maskuj wrażliwe wartości na granicy serializacji
- Sortowanie kluczy - deterministyczne wyjście dla kluczy cache, podpisów i asercji testów
- Obsługa BigInt - serializuj jako string, przywracaj przy parsowaniu
- Wykrywanie referencji cyklicznych - replacer oparty na WeakSet dla bezpiecznej serializacji
- Pretty-print ze zwartymi tablicami - wyjście czytelne dla człowieka bez nadmiaru białych znaków
- Streaming dużego JSON - parsowanie przyrostowe dla dużych plików i odpowiedzi API
Aby uzyskać pełną dokumentację argumentów replacer i space JSON.stringify, zobacz referencję MDN JSON.stringify. Dla nowoczesnej alternatywy deep clone zobacz dokumentację structuredClone(), która pokrywa wszystkie obsługiwane typy i przypadki brzegowe.