Галоўная / Артыкулы / Чаго JSON.stringify таямніча прыкідае, калі ператварае та чаго не праграмаваць у формат JSON.

Чаго JSON.stringify таямніча прыкідае, калі ператварае та чаго не праграмаваць у формат JSON.

Дазвольце дакласці, якія значэння JavaScriptа метод JSON.stringify прыкідае або зменяе, як toJSON, заменнікі та функцыі вярнення выправляюць гэта, і калі structuredClone є лепшым інструментам.

1602 слоў

JSON.stringify рэдкасцю выказвае проблемы. Калі ён стаўляецца да значэння, якое не можа представіць, ён зазвычай яго праўяе або ператварае ў іншае і продовжвае работу, вяртаючы абсалютна правільны JSON. Самэ так і з’яўляюцца багі, якія знаходзяцца за калькамі ад сваёй прычыны: данні зникаюць пад час серыялізаціі, але проблема проявляецца пазней у кодзе, які вообща не знае пра тое, што адбылася серыялізація. У гэтым кярыянтары разлічвае, што зникае, што меняе тип і шта выклікае адказ, а пасля паказвае вбудованыя інструменты (toJSON, заменікі, функціі вярнення даных і structuredClone) , якія дазволяюць контролаваць кожны случай намерна.

Атрыбуты, якія зникаюць без следа

Разглянем объект сэсіі, які мае сумешанне звычных дадзеных, функцію-калебэк, явна неякі атрыбут і сімбол:

const session = {
  userId: 42,
  role: "admin",
  onExpire: () => console.log("session expired"),
  lastActivity: undefined,
  tempToken: Symbol("temp"),
};

console.log(JSON.stringify(session));
// {"userId":42,"role":"admin"}

У рэзультате выкліку ёсць толькі два з пяці атрыбутаў. onExpire, lastActivity і tempToken з’явіліся, без жадных асабліваў, паведамленняў пра адхыленні і без нічога ў отрыманай строчце, што могла б парадкаваць пра ўдаленне чаго-небудзь. Унутры об’екта JSON.stringify праходзіць праз атрыбуты, значэння якіх є undefined, функцыяй або Symbol. Ніхто з іх не мае представлення ў формате JSON, і замест таго, каб выйсці з працы, серыялізатор вяртае правільны JSON для таго, што засталося.

Наследкі з’являюцца тонкая. Корыстнік, який парсуе гэты стрынг і спакоўваецца, што lastActivity будзе ўсё-такі наявны, нават якщо його значэнне undefined, не знайдзе яго. Ключ не ў порожнім стане; ён ніколі не патрапляў у выходны результат. Код, які розразняе стан „выключаны“ і „наявны, але undefined“, напрыклад за дапамогою "lastActivity" in obj або Object.keys, тепер будзе працаваць інакш.

Масівы працуюць інакш

Тыя ж не падтрымваныя значэння атрымуюць разныя обработкі ў масіве:

console.log(JSON.stringify([undefined, function () {}, 1]));
// [null,null,1]

Хутчэй яны стаюць null, а не зникаюць. Масіва нельга ўдаліць элемент без перасунування індэксаў усіх элементаў пасля яго, таму серыязавальнік заставляе гэта месца і запоўняе яго тым, што ў JSON супадае з „нічым“. Основная прычына ідэнтычная, але залежна ад таго, чыі значэнне знаходзілася ў об’екте чы ў масіве, спостерагаюцца два розныя бесшумных падходы. Два супакойлівых кейса таксама парадкаваныя: NaN і Infinity серыязуюцца як null, а вызов JSON.stringify безпосередна на undefined чы функцыі вяртае undefined, а не строку.

Даты вяртаюцца як строкі

Здаецца, што даты перазнаходзяюцься пасля перавозкі, але толькі напалову:

const record = { createdAt: new Date() };
const json = JSON.stringify(record);
console.log(json); // {"createdAt":"2026-09-07T14:30:00.000Z"}

const restored = JSON.parse(json);
console.log(restored.createdAt instanceof Date); // false
console.log(typeof restored.createdAt);          // "string"

Сама інформацыя пра дату застаецца недзейна; яна ўжо ў выходных даных у вигляде строкі ISO 8601. Але губіцца ўпорядак даных. Функцыя JSON.parse не можа з’ясаваць, чы раней была використана строка типу Date, чы проста текст, які выглядае як такі, таму ёй вяртаецца строка, і яна застаецца строкай, пакуль не будзе пераканвертавана.

Гэта мае значэнне, калі пасля такой пераканвертавання код вызывае методы, якія працуюць з датамі. Напрыклад, record.createdAt.getFullYear() выклекае адзінак, не таму што даныя некоректныя, а таму што ўпорядак іх заўсёды змініўся. Гэта часта трапляецца з адпаведзямі API, значэннямі, восстанавленымі з localStorage, і паведамленнямі, якія перадаюцца через очэредзі.

Цыклічныя структуры таксама выклекаюць адзінаки

Не кожны бяг нешумны. Дзеяныя об’екты вообща немагчыма серіялізаваць, і сэрвіс гучна пра гэта паведамляе:

const parent = { name: "parent" };
const child = { name: "child", parent };
parent.child = child;

JSON.stringify(parent); // TypeError: Converting circular structure to JSON

Для стварэння своего выходнага тэксту JSON.stringify працюе з графам объектаў. Калі які-небудзь об’ект у графе вяртаецца да аднаго з яго предкаў, такі процес ніколі не завершыцца. У замест на безмежную рекурсію, механізм фіксуе цыкл і негайна выклекчвае TypeError. Оін яўляецца аднам з некалькіх месцаў, дзе серыялізатор чыста не можа выконаць сваю задачу, і гэта мае вялікую прычыну: не існуе частковай або апрыксиматычной адпаведзі, таму што цыклічны граф сапраўды не можа быць прытуплены ў дрэво JSON. Значэнні BigInt таксама ўзмоцнаюць гэтую проблему; яны выклекчваюць TypeError, якщо толькі вы сабе іх не пераканвертуеце.

Контроль выходнага тэксту за дапамой toJSON

Дзеяныя типы ўжо самі вялікі, як ім следуе выглядаць у формате JSON, таму Date ператвараецца на строчку ISO, а не на порожній об’ект. Будзь-які об’екты можаць асаблівацца такой жа поведэнкай, задаўшы метод toJSON. Калі такі метод існуе, JSON.stringify вызывае яго і серыялізуе значэнне, якое ён вяртае, замест таго, каб выкарыстоўваць сопныя атрыбуты об’екта:

class Money {
  constructor(cents) {
    this.cents = cents;
  }
  toJSON() {
    return { amount: this.cents / 100, currency: "USD" };
  }
}

const price = new Money(3499);
console.log(JSON.stringify({ price })); // {"price":{"amount":34.99,"currency":"USD"}}

Без toJSON экземпляр Money будзе запісаны ў сваёй внутранней форме, {"cents":3499}, чым будуць адкрыты деталі рэалізацыі, на якія іншыя часткі системы ніколі не должны былі рэгуляравацца. За дапамогою toJSON об’ект сам визначае свою публічную рэпрэзентацыю. Date выкарыстоўвае самэ гэты механізм: Date.prototype.toJSON і є тым, што стварае строчку ISO.

Пам’ятайце, што гэта ўнівектарны процес. Аналіз выходных дадзеных дае вам звычны об’ект з amount і currency, а не экземпляр Money; практычна перзбудова класу ўскладнена задачай, якая адбываецца за дапамою функціи вякроўчання, описанай далей.

Функціі замены і вякроўчання: формаванне дадзеных у обох направленнях

JSON.stringify прыймае неабяжны другі аргумент — функцію замены, якая вызываецца для кожнага ключа і значэння пры ўрабатцы. Вернуць undefined з яе означае выдаленне данага элемента, а вернуць будзь-што інша — замену гэтага значэння. Гэта ўмоўлівае спосаб перадзвярнуць фільтрацыю чутлівых поль пры выходзе:

const user = { id: 1, name: "Priya", passwordHash: "a1b2c3..." };

const safe = JSON.stringify(user, (key, value) => {
  return key === "passwordHash" ? undefined : value;
});
console.log(safe); // {"id":1,"name":"Priya"}

JSON.parse прыносіць адпаведны рэшт: функцію вякроўчання, якая вызываецца для кожнага ключа і значэння пасля аналізу. Гэта ўзрадзеная методыка для рашэння проблемы з Date, паказанай раней:

const restored = JSON.parse(json, (key, value) => {
  if (key === "createdAt") return new Date(value);
  return value;
});
console.log(restored.createdAt instanceof Date); // true

Есць два моменты, якія варта знати. Функцыі адзначэння значэнняў працуют з ніжнага ўверх, таму заляглыя значэнняў вже адзначаюцца, калі обрабляецца ўсё іхнёе родныя аб’ект. А перакрыцчэ на назву ключа дазволяе знайсці гэты ключ на будзь-яй глыбіні, таму палец createdAt, які знаходзіцца ў якомнебудзь заляглым аб’екте, таксама будзе перакрыты; якщо гэтага вы не хачаце, трэба таксама перакрыць формат значэння.

Ні адна з гэтых прыемаў не ёсць рэдкім аспектам API. Гэта яшчэ той спосаб, які задуманы для точнага керавання серыялізацыёй, і ён набагато надзеяны лепшы, чым вычысцэнне атрыбутаў перад прыведэнням у строку або ручная палечка аб’ектаў пасля парсінгу.

Map і Set втрачаюць усё

Разработчыкі, якія спакульвуюць, што JSON будзе зберагаць любыя аб’екты, часта сталкнуцца з гэтым проблемам:

const tags = new Set(["urgent", "billing"]);
console.log(JSON.stringify({ tags })); // {"tags":{}}

Для сэріялізатора Set не ўважаецца ні масавым элементам, ні звычным об’ектам з перэлікам можна выявіць власныя атрыбуты, таму ён стае порожнім об’ектам, і всі значэння, якія ён меў, тыхо выкарыстоўваюцца. Map патрапляе у тое ж становішча. Якщо хоча бы адна з эйтанасоў павінна застацца, яе трэба явна пераканвертаваць пры практычнай ператворэнні ў строку, напрыклад, распрашчыўшы ёе ў масавы элемент:

const json = JSON.stringify({ tags: [...tags] }); // convert Set to array before stringifying

Для Map выразы [...map] або Object.fromEntries(map) ствараюць формы, якія можна сэріялізаваць, і функцыя вярнення може паўнасці первісную колекцыю пасля вярнення.

Глубокія копіі: выкорыстоўваце structuredClone

Працаваў гады JSON.parse(JSON.stringify(obj)) як распашчаты спосаб для глэбокага клонавання об’екта. Ён працуе толькі з випадку, і ён успадковвае всі вышэй описаныя обмежэння: функцыі і значэнні undefined зникаюць, даты ператвараюцца на строкі, колекцыі спрабоўваюць стаць пустымі, а цыркулярныя звязкі вызываюць памяткі.

Сучасныя браузеры і Node.js аддаюць спецыяльна створаную альтэрнатыўу:

const clone = structuredClone(original);

structuredClone адзейнае сапраўдны глыбокі копію за дапамой алгорытму структураванага клонавання. Ён зберагае об’екты Date, Map і Set, а таксама справляецца з цыркулярнымі рэферэнсамі, пры чым метод JSON або спакштуе іх, або адхіляе. Аднак у яго таксама є меры: ён выклікае падзею DataCloneError для функцый і вузлаў DOM, а экземпляры класаў вяртаюцца як звычныя об’екты без ўсіх ўластэйваў протатыпа. Калі мета — проста скопіяваць даны, structuredClone практычна завжды є кращым выборам. JSON.stringify ніколи не быў створаны для клонавання; ён быў проста настолькі зручны, што людзі вжывалі яго для гэтай меты.

Ключовыя выводы

  • JSON.stringify серыялізуе тую частку значэнняў JavaScript, якія можа представіць JSON, а не будзь-якія значэння JavaScript.
  • Унутрэ об’ектаў undefined, функцыі і сімвалы зникаюць; унутрэ масэў яны стаюць null.
  • Даты застаюцца у вачынку ISO, але трапляюцься без свайго типу; для ўвоскрэсцення іх трэба вжыць спецыяльны прыемак.
  • Ціркулярныя звязкі і BigInt вызываюць памылкі; Map і Set тыха сэріялізуюцца у {}.
  • Для задання публічнага формата об’екта, фільтраўвання выходных дадзеных і воскрэсцення типаў вжывайце toJSON, прыемак для замены дадзеных і спецыяльны прыемак для ўвоскрэсцення.
  • Калі трэба копія, вжывайце structuredClone, а JSON.stringify спрацоўвайце як фільтр, які форматуе дадзеныя, і явна керуйце ўсімі прызнакамі, калекцыямі та типамі, ўтамань нехай не зникаюць між разнымі часткамі вашай системы.
  • Супаўзеяныя матэрыялы

  • Што на самай працо выканае npm install: рэжыстры, package.json і Lockfiles — практычны апуснік па npm: рэжыстры і CLI, як npm install выкаанае завантажэнне пакетаў, што фіксуе package.json і package-lock.json, а таксама як публікуваць пакеты.