Как вывести циклическую структуру в формате, похожем на JSON?
Я столкнулся с проблемой при попытке преобразовать большой объект в формат JSON для отправки. Дело в том, что у него есть циклическая структура, и поэтому, когда я пытаюсь использовать JSON.stringify()
, я получаю одну из следующих ошибок:
TypeError: Converting circular structure to JSON
или
TypeError: cyclic object value
Мне нужно избавиться от всех циклических ссылок и получить строку, содержащую лишь те данные, которые можно сериализовать. Как мне это сделать?
Спасибо!
Пример объекта, с которым я работаю:
var obj = {
a: "foo",
b: obj
}
В результате я хочу получить следующий JSON:
{"a":"foo"}
5 ответ(ов)
Используйте JSON.stringify
с пользовательским заменителем. Например:
// Демонстрация: Циклическая ссылка
var circ = {};
circ.circ = circ;
// Обратите внимание: кэш не должен повторно использоваться при многократных вызовах JSON.stringify.
var cache = [];
JSON.stringify(circ, (key, value) => {
if (typeof value === 'object' && value !== null) {
// Найдена дублирующая ссылка, игнорируем ключ
if (cache.includes(value)) return;
// Сохраняем значение в нашей коллекции
cache.push(value);
}
return value;
});
cache = null; // Включаем сборку мусора
Заменитель в этом примере не является 100% корректным (в зависимости от вашего определения "дубликата"). В следующем случае значение будетdiscarded:
var a = {b:1}
var o = {};
o.one = a;
o.two = a;
// one и two ссылаются на один и тот же объект, но two будет проигнорирован:
JSON.stringify(o, ...);
Но концепция остается верной: используйте пользовательский заменитель и отслеживайте значения парсируемого объекта.
В качестве утилиты, написанной на ES6:
// безопасно обрабатывает циклические ссылки
JSON.safeStringify = (obj, indent = 2) => {
let cache = [];
const retVal = JSON.stringify(
obj,
(key, value) =>
typeof value === "object" && value !== null
? cache.includes(value)
? undefined // Найдена дублирующая ссылка, игнорируем ключ
: cache.push(value) && value // Сохраняем значение в нашей коллекции
: value,
indent
);
cache = null;
return retVal;
};
// Пример:
console.log('options', JSON.safeStringify(options));
Я действительно понравилось решение пользователя Trindaz — оно более детализированное, однако в нем были некоторые ошибки. Я исправил их для тех, кому это тоже понравится.
Кроме того, я добавил ограничение по длине для объектов в кэше.
Если объект, который я печатаю, действительно большой — я имею в виду бесконечно большой — я хочу ограничить свой алгоритм.
Вот исправленный код:
JSON.stringifyOnce = function(obj, replacer, indent){
var printedObjects = [];
var printedObjectKeys = [];
function printOnceReplacer(key, value){
if ( printedObjects.length > 2000){ // в браузерах не печатается более 20K объектов, я не вижу смысла разрешать 2K.. алгоритм и так не будет быстрым, если у нас слишком много объектов
return 'объект слишком длинный';
}
var printedObjIndex = false;
printedObjects.forEach(function(obj, index){
if(obj===value){
printedObjIndex = index;
}
});
if ( key == ''){ // корневой элемент
printedObjects.push(obj);
printedObjectKeys.push("root");
return value;
}
else if(printedObjIndex+"" != "false" && typeof(value)=="object"){
if ( printedObjectKeys[printedObjIndex] == "root"){
return "(указатель на корень)";
}else{
return "(см. " + ((!!value && !!value.constructor) ? value.constructor.name.toLowerCase() : typeof(value)) + " с ключом " + printedObjectKeys[printedObjIndex] + ")";
}
}else{
var qualifiedKey = key || "(пустой ключ)";
printedObjects.push(value);
printedObjectKeys.push(qualifiedKey);
if(replacer){
return replacer(key, value);
}else{
return value;
}
}
}
return JSON.stringify(obj, printOnceReplacer, indent);
};
Надеюсь, это поможет другим!
Ответ @RobW верный, но это более производительное решение! Используется хэш-таблица/множество:
const customStringify = function (v) {
const cache = new Set();
return JSON.stringify(v, function (key, value) {
if (typeof value === 'object' && value !== null) {
if (cache.has(value)) {
// Обнаружена циклическая ссылка
try {
// Если это значение не ссылается на родителя, его можно убрать дубликаты
return JSON.parse(JSON.stringify(value));
}
catch (err) {
// Отбрасываем ключ, если значение не может быть избавлено от дубликатов
return;
}
}
// Сохраняем значение в наше множество
cache.add(value);
}
return value;
});
};
Этот код использует множество для отслеживания уже сериализованных объектов, что позволяет избежать циклических ссылок и повышает производительность по сравнению с другими методами.
Если вы ищете решение проблемы с круговыми ссылками в JavaScript, и заранее не знаете все ключи, которые могут их вызвать, вы можете использовать обертку вокруг функции JSON.stringify
, чтобы исключить такие ссылки. Пример такого скрипта доступен по ссылке: https://gist.github.com/4653128.
Суть решения заключается в том, чтобы хранить ссылки на уже обработанные объекты в массиве и проверять это в функции замены (replacer), прежде чем возвращать значение. Это более строгий подход, чем просто исключение круговых ссылок, так как он также предотвращает повторное вывод мультипликации одного и того же объекта, что в свою очередь помогает избежать круговых ссылок.
Пример обертки:
function stringifyOnce(obj, replacer, indent){
var printedObjects = [];
var printedObjectKeys = [];
function printOnceReplacer(key, value){
var printedObjIndex = false;
printedObjects.forEach(function(obj, index){
if(obj === value){
printedObjIndex = index;
}
});
if(printedObjIndex && typeof(value) === "object"){
return "(см. " + value.constructor.name.toLowerCase() + " с ключом " + printedObjectKeys[printedObjIndex] + ")";
} else {
var qualifiedKey = key || "(пустой ключ)";
printedObjects.push(value);
printedObjectKeys.push(qualifiedKey);
if(replacer){
return replacer(key, value);
} else {
return value;
}
}
}
return JSON.stringify(obj, printOnceReplacer, indent);
}
Таким образом, использование этой обертки поможет вам избежать проблем с круговыми ссылками и обеспечит, что объект будет сериализован только один раз.
Ваша функция preventCircularJson
удаляет циклические ссылки из объектов JavaScript, что позволяет корректно сериализовать их в JSON.
В вашем примере:
var a = { b: "b" };
a.a = a; // Создаем циклическую ссылку
JSON.stringify(preventCircularJson(a));
Результат выполнения JSON.stringify(preventCircularJson(a));
будет:
"{"b":"b","a":"CIRCULAR_REFERENCE_REMOVED"}"
Это означает, что когда происходит попытка сериализовать объект a
, содержащий циклическую ссылку (где свойство a
ссылается на сам объект a
), функция preventCircularJson
заменяет это свойство на строку "CIRCULAR_REFERENCE_REMOVED"
.
Функция работает следующим образом:
- Инициализирует массив
censorTheseItems
, который будет использоваться для отслеживанияVisited items. - Проходит по всем свойствам исходного объекта.
- Если значение свойства — это объект, оно добавляется в
recursiveItems
для дальнейшей обработки. - Простой тип значений копируется без изменений в новый объект
ret
. - Создает список
censorChildItems
, который содержит все объекты, которые необходимо отметить как уже посещенные. - Для каждого свойства в
recursiveItems
проверяет, было ли значение уже посещено. Если да, то заменяет его наcensoredMessage
. - Если объект еще не был посещен, вызывается рекурсивно
preventCircularJson
, и результат помещается вret
.
В результате вы получаете объект без циклических ссылок, что позволяет успешно сериализовать его в строку JSON.
Как разобрать JSON с помощью Node.js? [закрыто]
Как прочитать JSON-файл в память сервера?
Преобразование объекта JS в строку JSON
Как сравнить массивы в JavaScript?
Функция map для объектов (вместо массивов)