8

Как вывести циклическую структуру в формате, похожем на JSON?

11

Я столкнулся с проблемой при попытке преобразовать большой объект в формат JSON для отправки. Дело в том, что у него есть циклическая структура, и поэтому, когда я пытаюсь использовать JSON.stringify(), я получаю одну из следующих ошибок:

TypeError: Converting circular structure to JSON

или

TypeError: cyclic object value

Мне нужно избавиться от всех циклических ссылок и получить строку, содержащую лишь те данные, которые можно сериализовать. Как мне это сделать?

Спасибо!

Пример объекта, с которым я работаю:

var obj = {
  a: "foo",
  b: obj
}

В результате я хочу получить следующий JSON:

{"a":"foo"}

5 ответ(ов)

7

Используйте 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));
0

Я действительно понравилось решение пользователя 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);
};

Надеюсь, это поможет другим!

0

Ответ @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;
  });
};

Этот код использует множество для отслеживания уже сериализованных объектов, что позволяет избежать циклических ссылок и повышает производительность по сравнению с другими методами.

0

Если вы ищете решение проблемы с круговыми ссылками в 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);
}

Таким образом, использование этой обертки поможет вам избежать проблем с круговыми ссылками и обеспечит, что объект будет сериализован только один раз.

0

Ваша функция 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".

Функция работает следующим образом:

  1. Инициализирует массив censorTheseItems, который будет использоваться для отслеживанияVisited items.
  2. Проходит по всем свойствам исходного объекта.
  3. Если значение свойства — это объект, оно добавляется в recursiveItems для дальнейшей обработки.
  4. Простой тип значений копируется без изменений в новый объект ret.
  5. Создает список censorChildItems, который содержит все объекты, которые необходимо отметить как уже посещенные.
  6. Для каждого свойства в recursiveItems проверяет, было ли значение уже посещено. Если да, то заменяет его на censoredMessage.
  7. Если объект еще не был посещен, вызывается рекурсивно preventCircularJson, и результат помещается в ret.

В результате вы получаете объект без циклических ссылок, что позволяет успешно сериализовать его в строку JSON.

Чтобы ответить на вопрос, пожалуйста, войдите или зарегистрируйтесь