JSON и данные
Синтаксис и правила JSON, JSON.stringify и JSON.parse с replacer/reviver, глубокое клонирование, structuredClone и опасности eval.
10 вопросов
JuniorТеорияЧастоЧто такое JSON и какие типы значений может содержать JSON-документ?
Что такое JSON и какие типы значений может содержать JSON-документ?
JSON — это независимый от языка текстовый формат для структурированных данных. Он допускает объекты со строковыми ключами в двойных кавычках, массивы, строки, конечные числа, true, false и null. Он не может представить функции, undefined, символы, BigInt, даты и комментарии, а каждый ключ и строка обязаны быть в двойных кавычках.
Типичные ошибки
- ✗Использовать одинарные кавычки или ключи без кавычек, что JSON отвергает — допустимы только двойные
- ✗Считать, что JSON поддерживает комментарии или висящие запятые как литерал JavaScript
- ✗Думать, что
undefined, функции или даты имеют нативное представление в JSON
Уточняющие вопросы
- →Как обычно передают
Dateчерез JSON, если у формата нет типа даты? - →Почему JSON называют подмножеством синтаксиса литерала объекта, а не идентичным ему?
JuniorТеорияЧастоЧто делает JSON.parse и что происходит при некорректном вводе JSON?
Что делает JSON.parse и что происходит при некорректном вводе JSON?
JSON.parse превращает JSON-строку в соответствующее значение JavaScript (объект, массив, строку, число, булево или null). Если текст не является валидным JSON — неверные кавычки, висящая запятая, undefined, лишний токен — он выбрасывает SyntaxError, а не возвращает null, поэтому вызовы на недоверенном вводе стоит оборачивать в try/catch.
Типичные ошибки
- ✗Ожидать, что
JSON.parseвернётnullна плохом вводе, а не выброситSyntaxError - ✗Разбирать недоверенные данные без
try/catch, из-за чего испорченный ввод роняет код - ✗Думать, что
JSON.parseпринимает одинарные кавычки или висящие запятые как литерал JavaScript
Уточняющие вопросы
- →Как второй аргумент
reviverпозволяет преобразовывать значения при разборе? - →Почему
JSON.parseбезопаснееevalдля превращения текста в значение?
JuniorТеорияЧастоЧто делает JSON.stringify с функциями, undefined, NaN и Infinity?
Что делает JSON.stringify с функциями, undefined, NaN и Infinity?
JSON.stringify сериализует значение в JSON-строку. В объектах он молча отбрасывает ключи, значение которых — функция, undefined или символ; в массивах они становятся null. NaN и Infinity сериализуются в null. На верхнем уровне undefined или функция дают undefined (не строку), а не текст 'undefined'.
Типичные ошибки
- ✗Ожидать, что
JSON.stringifyвыбросит ошибку наundefinedили функциях, а не отбросит их - ✗Думать, что
NaNиInfinityсохранятся как числа, а не станутnull - ✗Считать, что верхнеуровневый
undefinedвернёт строку'undefined', а неundefined
Уточняющие вопросы
- →Почему дырка в массиве или
undefinedв массиве становитсяnull, а ключ объекта отбрасывается? - →Как функция
replacerможет изменить, какие значения сериализуются?
MiddleТеорияЧастоКак работает клон JSON.parse(JSON.stringify(x)) и каковы его подводные камни?
Как работает клон JSON.parse(JSON.stringify(x)) и каковы его подводные камни?
Сериализация в JSON и обратный разбор дают глубокую копию без общих ссылок для простых данных. Но цикл проходят только JSON-представимые значения: функции, undefined и символы отбрасываются, Date становится строкой, Map/Set становятся {}, NaN/Infinity становятся null, BigInt выбрасывает ошибку, а циклическая ссылка выбрасывает TypeError.
Типичные ошибки
- ✗Называть это поверхностной копией — он глубоко копирует все JSON-представимые вложенные данные
- ✗Считать, что
Date,Map,Setили функции переживают цикл нетронутыми - ✗Забывать, что циклическая ссылка заставляет
JSON.stringifyвыброситьTypeError
Уточняющие вопросы
- →Какой встроенный API глубоко клонирует циклы и объекты
Date, что не под силу JSON-трюку? - →Почему
Dateвозвращается строкой, а неDate, после цикла?
JuniorТеорияИногдаПочему eval считают опасным и что использовать вместо него?
Почему eval считают опасным и что использовать вместо него?
eval выполняет произвольную строку как код с правами вызывающего, поэтому недоверенный ввод превращается в дыру для инъекции кода. Он также мешает оптимизации движка, может читать и писать окружающую область видимости и затрудняет понимание кода. Чтобы превратить текст в данные, используйте JSON.parse; для динамических ключей — обращение через скобки.
Типичные ошибки
- ✗Считать, что
evalработает в песочнице, а не с полной областью видимости и правами вызывающего - ✗Использовать
evalдля разбора данных, когдаJSON.parse— безопасный, специально созданный инструмент - ✗Хвататься за
evalдля чтения динамического свойства вместо обращения через скобки
Уточняющие вопросы
- →Как доступ
evalк окружающей области видимости блокирует оптимизации движка? - →Когда
JSON.parseне может заменить вызовeval, и что это говорит о вводе?
MiddleТеорияИногдаКакие значения JavaScript JSON не может представить и как каждое из них ведёт себя?
Какие значения JavaScript JSON не может представить и как каждое из них ведёт себя?
JSON не может закодировать несколько значений JavaScript. Циклическая ссылка заставляет JSON.stringify выбросить TypeError, как и BigInt. undefined, функции и символы отбрасываются как ключи объекта и становятся null в массивах. NaN и Infinity сериализуются в null. У Date нет нативного типа, и она превращается в строку через свой toJSON.
Типичные ошибки
- ✗Ожидать, что циклическая ссылка обработается аккуратно, а не выбросит ошибку
- ✗Считать, что
BigIntсериализуется в число, а не выбрасываетTypeError - ✗Думать, что
NaNиInfinityсохраняются как числа, а не становятсяnull
Уточняющие вопросы
- →Как сериализовать
BigInt, несмотря на то чтоJSON.stringifyпо умолчанию на нём выбрасывает ошибку? - →Почему циклическая ссылка выбрасывает ошибку, а глубоко вложенный, но ацикличный объект сериализуется нормально?
MiddleТеорияИногдаКак аргументы replacer, toJSON() и отступ влияют на вывод JSON.stringify?
Как аргументы replacer, toJSON() и отступ влияют на вывод JSON.stringify?
JSON.stringify(value, replacer, space) позволяет настроить вывод. Функция replacer преобразует или отбрасывает каждую пару ключ/значение; массив replacer оставляет в белом списке только указанные ключи. Если у значения есть метод toJSON(), сериализуется его возвращаемое значение. Третий аргумент space (число или строка) форматирует вывод с этим отступом.
Типичные ошибки
- ✗Думать, что массив
replacerзадаёт значения по умолчанию, а не белый список оставляемых ключей - ✗Считать, что функция
replacerдолжна возвращать булево, а не преобразованное значение - ✗Забывать, что
toJSON()позволяет значению управлять своей сериализованной формой (как уDate)
Уточняющие вопросы
- →В каком порядке движок применяет
toJSON()иreplacerк одному значению? - →Как использовать функцию
replacer, чтобы скрыть чувствительные ключи перед логированием?
SeniorТеорияИногдаЧем structuredClone, JSON-цикл и ручное клонирование различаются по тому, что они обрабатывают?
Чем structuredClone, JSON-цикл и ручное клонирование различаются по тому, что они обрабатывают?
structuredClone глубоко копирует циклические графы, Date, RegExp, Map, Set и типизированные массивы, но выбрасывает ошибку на функциях и узлах DOM. JSON-цикл проще, но теряет данные: отбрасывает функции, ломается на циклах и BigInt и портит Date/Map. Ручное клонирование обрабатывает что угодно, включая функции, но эту логику нужно писать и поддерживать самому.
Типичные ошибки
- ✗Считать, что
structuredCloneведёт себя как JSON-цикл и теряетDate/Map - ✗Полагать, что
structuredCloneклонирует функции или узлы DOM — он выбрасывает на обоих - ✗Думать, что JSON-цикл может скопировать цикл, когда он выбрасывает
TypeError
Уточняющие вопросы
- →Почему
structuredCloneможет скопировать циклический граф, которыйJSON.stringifyотвергает? - →Когда ручное клонирование всё ещё необходимо, несмотря на существование
structuredClone?
MiddleТеорияРедкоЧто такое аргумент reviver у JSON.parse и для чего он нужен?
Что такое аргумент reviver у JSON.parse и для чего он нужен?
JSON.parse(text, reviver) передаёт функцию, вызываемую для каждой пары ключ/значение снизу вверх после разбора. Её возвращаемое значение заменяет разобранное; возврат undefined удаляет этот ключ. Обычно её используют, чтобы превратить сырые значения обратно в богатые типы — например, превратить ISO-строки дат в настоящие объекты Date при разборе.
Типичные ошибки
- ✗Думать, что
reviverвыполняется один раз на корне, а не для каждой вложенной пары - ✗Считать, что он препроцессит текст, а не преобразует уже разобранные значения
- ✗Не знать, что возврат
undefinedизreviverудаляет этот ключ
Уточняющие вопросы
- →Почему
reviverобходит значения снизу вверх (дети раньше родителя)? - →Как
reviverиtoJSON()на стороне сериализации вместе дают полный цикл дляDate?
SeniorТеорияРедкоКак спроектировать цикл toJSON/reviver, сохраняющий богатые типы вроде дат и Map?
Как спроектировать цикл toJSON/reviver, сохраняющий богатые типы вроде дат и Map?
Определите тегированную схему: при сериализации toJSON или replacer превращает каждое богатое значение в обычный JSON-объект с маркером типа и сериализуемой полезной нагрузкой, например { __type: 'Map', value: [...] }. При разборе reviver сопоставляет этот маркер и восстанавливает исходный тип. Соглашение о маркере — это контракт, общий для обеих сторон, чтобы пережить теряющий данные слой JSON.
Типичные ошибки
- ✗Ожидать, что
JSON.stringifyсохранит конструкторы без явного маркера типа - ✗Ставить
toJSONиreviverне на те стороны цикла - ✗Кодировать богатые значения без тега, из-за чего
reviverне знает исходный тип
Уточняющие вопросы
- →Как ваша схема обработает вложенные богатые типы, например
Map, значения которой — объектыDate? - →Какой риск коллизии несёт маркер
__typeи как защитить от него реальные данные?