JSON: обмен данными
Здесь будет встроен видео-разбор темы (5–7 минут, YouTube / RuTube / VK Видео) — тем, кто лучше воспринимает на слух, текст ниже не обязателен.
Объект живёт в памяти и сам никуда не уедет: чтобы отправить данные по сети или положить в файл, нужен текст. JSON — общий для всех языков способ записать структуру строкой и собрать её обратно.
Внутри программы данные живут объектами и массивами. Но стоит отправить их по сети или сохранить в файл — и структура исчезает: наружу уходят только байты, то есть текст. JSON — договор о том, как записать структуру текстом, чтобы на другом конце её собрали обратно точно такой же.
Синтаксис JSON вырос из объектов JavaScript, но сам формат давно ничей: его читают Python, Go, PHP, PostgreSQL и любой HTTP-клиент. Победил он скукой — шесть типов значений (объект, массив, строка, число, true/false, null) и никаких схем, тегов и атрибутов, как в XML. Правила при этом жёсткие: ключи и строки только в двойных кавычках, комментариев нет, висячая запятая запрещена.
JSON.stringify берёт значение и возвращает строку. Вглубь он спускается сам: вложенные объекты и массивы обрабатываются рекурсивно, склеивать текст вручную не нужно.
Заметьте: в результате нет ни одного лишнего пробела, а ключи стоят в кавычках, хотя в исходном объекте кавычек не было. Это не вопрос стиля — так требует формат.
Если в структуре есть цикл — объект прямо или через цепочку ссылается на самого себя, — рекурсия ушла бы в бесконечность. Поэтому JSON.stringify сдаётся и бросает ошибку.
JSON.parse делает обратное: читает строку и строит из неё значение. Обратные кавычки в примере — просто способ написать строку с двойными кавычками внутри, к JSON они отношения не имеют.
Слово «строит» здесь важное: получается совершенно новое значение, никак не связанное с исходным объектом. На этом держится старый трюк глубокого копирования — объект прогоняют в текст и обратно.
Копия через JSON — трюк на скорую руку. Для честной копии сегодня есть structuredClone: он умеет Date, Map и Set.
JSON знает только свои шесть типов, поэтому всё остальное он либо выбрасывает, либо превращает во что-то похожее — молча, без предупреждений.
| Что в JavaScript | Что попадёт в JSON |
|---|---|
| функция | свойство исчезает |
| undefined | свойство исчезает, а в массиве становится null |
| Date | строка в формате ISO |
| NaN, Infinity | null |
| Map, Set | пустой объект {} |
| BigInt | ошибка TypeError |
Отдельная ловушка — даты. Date уезжает читаемой ISO-строкой, но обратно строка датой не становится: разбор вернёт именно строку, и getFullYear у неё уже не спросишь.
Третий аргумент JSON.stringify — отступ. Число означает столько пробелов на уровень, строка (например "\t") — свой символ отступа. Второй аргумент при этом обычно не нужен, поэтому на его месте ставят null.
Второй аргумент — replacer, фильтр. Массив строк оставит только перечисленные ключи; функция вызывается для каждой пары и решает, что записать. Удобно, когда в объекте есть то, чему не место в логах.
У JSON.parse тоже есть второй аргумент — reviver. Он вызывается для каждой пары «ключ — значение», и то, что вернули, попадает в результат. Это штатный способ вернуть строкам их настоящий тип.
Строка, пришедшая извне, — это не обещание корректного JSON. Пустой ответ, HTML страницы ошибки вместо данных, лишняя запятая — и JSON.parse бросает SyntaxError, а без обработки падает весь код вокруг. Перехватить такую ошибку помогает конструкция try...catch: код, который может сломаться, кладут в блок try, и если там брошена ошибка, выполнение не обрывается, а переходит в блок catch. Туда же приходит объект ошибки, и по его полю name видно, что случилось. Подробно конструкцию разбирает статья «try..catch: перехват ошибок».
Поэтому чужой текст разбирают в try...catch. Чтобы не повторять одно и то же в каждом месте, заводят маленькую обёртку с запасным значением. Объект ошибки ей не нужен, поэтому скобки после catch опущены — так тоже можно.
- JSON — текстовый формат обмена данными: структура уходит строкой и собирается обратно, причём в любом языке.
- JSON.stringify превращает значение в строку, JSON.parse строит из строки новое значение — не связанное с исходным.
- Функции, undefined, Map и Set сериализацию не переживают, NaN становится null, а Date уезжает строкой и сама датой не возвращается.
- Третий аргумент stringify — отступ для читаемого вывода, второй — replacer-фильтр; у parse второй аргумент — reviver, который умеет чинить типы.
- На некорректном тексте JSON.parse бросает SyntaxError, поэтому данные из сети разбирают в try...catch.
1/3Что выведет console.log(JSON.stringify({ a: undefined, b: 1 }))?
2/3Объект с полем Date прогнали через JSON.stringify, а потом через JSON.parse. Чем окажется это поле?
3/3За что отвечает третий аргумент JSON.stringify?
Довезти данные целыми
Объект состояния сериализуется с потерями: посмотрите, что стало с полем visited. Приведите данные к тому, что JSON умеет, и получите строку, в которой уцелели все три поля.
Set для JSON — обычный объект без собственных свойств, отсюда пустые фигурные скобки. Дата выживает только потому, что у Date есть метод toJSON. Значит, перед сериализацией собираем «плоскую» копию из того, что формат понимает.
Тот же результат даёт метод toJSON у самого объекта: если он есть, JSON.stringify вызовет его и запишет возвращённое значение. И помните про обратный путь — из массива Set придётся собирать самим.
Спрятать пароли в логе
Этот объект уходит в лог целиком, вместе с паролями — в том числе вложенными. Сделайте так, чтобы вместо любого значения ключа password на любой глубине попадало "***". Остальные поля должны остаться нетронутыми.
Пишем replacer-функцию. Её вызывают для каждой пары на каждом уровне вложенности, поэтому рекурсию писать не нужно — достаточно проверить ключ и вернуть либо заглушку, либо исходное значение.
Важно вернуть value в ветке «иначе»: если функция вернёт undefined, свойство просто исчезнет из результата. Кстати, самый первый вызов приходит с пустым ключом и всем объектом целиком — это нормально.