Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Json.parse та json.stringify в JavaScript». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**JSON.stringify()** перетворює JavaScript-значення на JSON-рядок; **JSON.parse()** перетворює JSON-рядок назад на JavaScript-значення. ```javascript const obj = { name: 'Alice', age: 30 }; const str = JSON.stringify(obj); // '{"name":"Alice","age":30}' const back = JSON.parse(str); // { name: 'Alice', age: 30 } ``` **Головне:** stringify тихо відкидає функції та Symbol; parse кидає `SyntaxError` на некоректному JSON.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**JSON.stringify()** перетворює JavaScript-значення на JSON-рядок. **JSON.parse()** перетворює JSON-рядок назад на JavaScript-значення. ## Теорія ### TL;DR - `stringify` "пакує" об'єкт у звичайний текстовий рядок для передачі; `parse` "розпаковує" його назад у робочий об'єкт. - `stringify` тихо відкидає функції, `undefined` та Symbol; Dates стають ISO-рядками; `BigInt` одразу кидає `TypeError`. - `parse` перевіряє кожен символ за специфікацією RFC 8259 і кидає `SyntaxError` на будь-яких відхиленнях. - Використовуй обидва для: тіла API-запитів, localStorage, конфіг-файлів. - Уникай для: об'єктів Date, функцій, класів, циклічних посилань. ### Швидкий приклад ```javascript const user = { name: 'Alice', age: 30, active: true }; const json = JSON.stringify(user); // '{"name":"Alice","age":30,"active":true}' const parsed = JSON.parse(json); console.log(parsed.name); // 'Alice' console.log(typeof json); // 'string' console.log(typeof parsed); // 'object' ``` `stringify` повертає звичайний рядок без методів. `parse` повертає об'єкт, з яким можна нормально працювати. ### Ключова різниця `JSON.stringify()` будує рядок, рекурсивно обходячи значення та викликаючи `toJSON()` на об'єктах де він є. Все без JSON-еквівалента - функції, Symbol, `undefined` - відкидається з об'єктів або замінюється `null` у масивах. `JSON.parse()` робить зворотне, але перевіряє вхід за RFC 8259. Одинарні лапки, ключі без лапок, зайві коми - все це кидає `SyntaxError`. Round-trip не є повним: втрачені дані не повернеш через parse. ### Коли використовувати - API-запити: `JSON.stringify(body)` перед відправкою, `response.json()` після отримання. - localStorage: `setItem('key', JSON.stringify(data))` для збереження, `JSON.parse(getItem('key'))` для читання. - Дебаг-логи: `JSON.stringify(obj, null, 2)` для читабельного знімку стану. - Конфіг у Node.js: `JSON.parse(fs.readFileSync('config.json', 'utf8'))` при старті. - Уникай для: Date, функцій, класів, циклічних посилань, `BigInt`. ### Що stringify змінює або відкидає ```javascript const obj = { fn: () => 'hello', // відкидається з об'єкта undef: undefined, // відкидається з об'єкта sym: Symbol('id'), // відкидається з об'єкта date: new Date('2023-01-01'), // стає ISO-рядком nan: NaN, // стає null }; JSON.stringify(obj); // '{"date":"2023-01-01T00:00:00.000Z","nan":null}' // У масивах undefined та функції стають null: JSON.stringify([1, undefined, () => {}, 3]); // '[1,null,null,3]' // BigInt кидає одразу - жодної тихої конвертації: JSON.stringify({ id: 123n }); // TypeError: Do not know how to serialize a BigInt ``` ### replacer та reviver Обидва методи мають необов'язковий параметр для нестандартних сценаріїв. `replacer` у `stringify` фільтрує або трансформує значення на виході. `reviver` у `parse` відновлює типи на вході. ```javascript // replacer як масив: whitelist властивостей JSON.stringify({ name: 'Bob', password: 'secret' }, ['name']); // '{"name":"Bob"}' // replacer як функція: обробка BigInt JSON.stringify({ id: 123n }, (key, value) => typeof value === 'bigint' ? value.toString() : value ); // '{"id":"123"}' // reviver: відновити Date після parse const json = '{"name":"Alice","birthday":"1990-06-15T00:00:00.000Z"}'; const result = JSON.parse(json, (key, value) => key === 'birthday' ? new Date(value) : value ); console.log(result.birthday instanceof Date); // true ``` Replacer викликається зверху вниз від кореня; reviver - знизу вгору, спочатку вкладені властивості. ### Як це працює у V8 У V8 `JSON.stringify()` використовує C++ серіалізатор, який обходить граф об'єктів, записує примітиви напряму і викликає `toJSON()` де він є. `JSON.parse()` - це рекурсивний descent-парсер, що перевіряє UTF-8 та синтаксис за RFC 8259. Обидва методи працюють у нативному коді. Тому вони швидші за будь-яку бібліотеку для JSON написану на JavaScript. ### Типові помилки **1. Забути зробити parse після читання з localStorage** ```javascript localStorage.setItem('user', JSON.stringify({ name: 'Alice' })); // Неправильно const raw = localStorage.getItem('user'); console.log(raw.name); // undefined - raw це рядок, не об'єкт // Правильно const user = JSON.parse(localStorage.getItem('user')); console.log(user.name); // 'Alice' ``` Ця помилка - одна з найпоширеніших у продакшен-коді. localStorage завжди повертає рядки. **2. Очікувати, що Date переживе серіалізацію** ```javascript const obj = { birthday: new Date('1990-01-01') }; const clone = JSON.parse(JSON.stringify(obj)); console.log(clone.birthday instanceof Date); // false console.log(clone.birthday); // '1990-01-01T00:00:00.000Z' // clone.birthday.getFullYear() → TypeError // Виправлення: reviver const restored = JSON.parse(JSON.stringify(obj), (k, v) => k === 'birthday' ? new Date(v) : v ); console.log(restored.birthday instanceof Date); // true ``` **3. Циклічні посилання ламають stringify** ```javascript const obj = { x: 1 }; obj.self = obj; JSON.stringify(obj); // TypeError: Converting circular structure to JSON // Виправлення: replacer з WeakSet function safeStringify(value) { const seen = new WeakSet(); return JSON.stringify(value, (key, val) => { if (typeof val === 'object' && val !== null) { if (seen.has(val)) return '[Circular]'; seen.add(val); } return val; }); } ``` **4. Передати не-рядок у JSON.parse()** ```javascript JSON.parse({ a: 1 }); // SyntaxError // Об'єкт зводиться до рядка "[object Object]", який не є валідним JSON. // Передавай тільки рядок: JSON.parse('{"a":1}'); // { a: 1 } ``` **5. Використовувати JSON для deep clone при наявності спеціальних типів** ```javascript const original = { date: new Date(), map: new Map([[1, 'one']]) }; const broken = JSON.parse(JSON.stringify(original)); // broken.date - рядок, broken.map - порожній об'єкт {} // Краще використати structuredClone(): const proper = structuredClone(original); console.log(proper.date instanceof Date); // true console.log(proper.map instanceof Map); // true ``` ### Де зустрічається - React: збереження стану в localStorage через `stringify` при зміні, `parse` при монтуванні (типово для теми інтерфейсу в Next.js). - Express: `express.json()` автоматично парсить тіло JSON-запитів; `res.json()` автоматично серіалізує відповідь. - Node.js: `fs.writeFileSync('config.json', JSON.stringify(config, null, 2))` для читабельних конфіг-файлів. - Fetch API: `body: JSON.stringify(data)` у POST-запитах, `await response.json()` для відповіді. - Redux Persist: серіалізує зрізи стору в localStorage між сесіями. ### Питання на співбесіді **Q:** Що відбувається з `undefined` в об'єкті та в масиві? **A:** В об'єкті ключ відкидається повністю: `JSON.stringify({a: undefined})` дає `'{}'`. В масиві `undefined` стає `null`, щоб зберегти позиції елементів: `JSON.stringify([undefined])` дає `'[null]'`. **Q:** `replacer` приймає і функцію, і масив. Коли вибирати масив? **A:** Масив - для простого whitelist властивостей: `JSON.stringify(user, ['name', 'email'])`. Функція - коли потрібна умовна логіка або трансформація типів на вкладених рівнях. **Q:** Яка різниця між `JSON.parse()` та `eval()`? **A:** `eval()` виконує довільний JavaScript, що небезпечно з ненадійними даними. `JSON.parse()` приймає тільки валідний JSON і не може запускати код. Для зовнішніх даних завжди використовуй `JSON.parse()`. **Q:** Як `JSON.parse()` обробляє дублікати ключів у JSON-рядку? **A:** Перемагає останнє значення. `JSON.parse('{"a":1,"a":2}')` повертає `{a: 2}`. Специфікація дозволяє дублікати, але їх наявність завжди є помилкою в джерелі. **Q:** (Senior) Реалізуй stringify для циклічних посилань без бібліотек. **A:** Відстежуй відвідані об'єкти через `WeakSet`. У функції-replacer перевіряй чи значення вже є у множині перед додаванням. `WeakSet` підходить тут тому, що тримає посилання слабко і не блокує збирач сміття. ## Приклади ### localStorage: збереження та відновлення налаштувань ```javascript const settings = { theme: 'dark', fontSize: 16, notifications: true }; // Зберегти localStorage.setItem('settings', JSON.stringify(settings)); // Відновити (перевір на null - ключа може не бути) const raw = localStorage.getItem('settings'); const loaded = raw !== null ? JSON.parse(raw) : settings; console.log(loaded.theme); // 'dark' ``` Завжди перевіряй на `null` перед `JSON.parse()`. `localStorage.getItem()` повертає `null` для відсутніх ключів, а звернення до властивостей `null` пізніше кине помилку. ### POST-запит через Fetch ```javascript async function createUser(data) { const response = await fetch('/api/users', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) // об'єкт → рядок для мережі }); if (!response.ok) throw new Error(`HTTP ${response.status}`); return response.json(); // парсить тіло відповіді } createUser({ name: 'Alice', role: 'developer' }) .then(user => console.log('Створено з id:', user.id)); ``` `response.json()` - це фактично `JSON.parse(await response.text())`. Якщо тіло відповіді не є валідним JSON, метод кидає помилку. Тому спочатку перевіряй `response.ok`. ### replacer та reviver для нестандартних сценаріїв ```javascript // stringify: прибрати чутливі поля, обробити BigInt const payload = { userId: 9007199254740993n, // завеликий для звичайного JS-числа name: 'Bob', password: 'secret' }; const serialized = JSON.stringify(payload, (key, value) => { if (key === 'password') return undefined; // прибрати if (typeof value === 'bigint') return value.toString(); // конвертувати return value; }); // '{"userId":"9007199254740993","name":"Bob"}' // parse: відновити Date з відповіді API const apiResponse = '{"id":1,"createdAt":"2023-06-15T10:00:00.000Z"}'; const record = JSON.parse(apiResponse, (key, value) => key === 'createdAt' ? new Date(value) : value ); console.log(record.createdAt instanceof Date); // true console.log(record.createdAt.getFullYear()); // 2023 ```Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.