Обнаружение и исправление циклических ссылок в JavaScript

Учитывая, что у меня есть круговая ссылка в большом объекте JavaScript

И я пробую JSON.stringify(problematicObject)

И браузер бросает

"TypeError: преобразование круговой структуры в JSON"

(что ожидается)

Затем я хочу найти причину этой круглой ссылки, желательно, используя инструменты разработчика Chrome? Это возможно? Как найти и исправить круговые ссылки в большом объекте?

Ответы

Ответ 1

Вытянут из http://blog.vjeux.com/2011/javascript/cyclic-object-detection.html. Одна строка добавлена ​​для определения того, где находится цикл. Вставьте это в инструменты Chrome dev:

function isCyclic (obj) {
  var seenObjects = [];

  function detect (obj) {
    if (obj && typeof obj === 'object') {
      if (seenObjects.indexOf(obj) !== -1) {
        return true;
      }
      seenObjects.push(obj);
      for (var key in obj) {
        if (obj.hasOwnProperty(key) && detect(obj[key])) {
          console.log(obj, 'cycle at ' + key);
          return true;
        }
      }
    }
    return false;
  }

  return detect(obj);
}

Здесь тест:

> a = {}
> b = {}
> a.b = b; b.a = a;
> isCyclic(a)
  Object {a: Object}
   "cycle at a"
  Object {b: Object}
   "cycle at b"
  true

Ответ 2

Ответ @tmack - это определенно то, что я искал, когда нашел этот вопрос!

К сожалению, он возвращает много ложных срабатываний - он возвращает истину, если объект реплицируется в JSON, что отличается от цикличности. Круглость означает, что объект является его собственным потомком, например

obj.key1.key2.[...].keyX === obj

Я изменил исходный ответ, и это работает для меня:

function isCyclic(obj) {
  var keys = [];
  var stack = [];
  var stackSet = new Set();
  var detected = false;

  function detect(obj, key) {
    if (obj && typeof obj != 'object') { return; }

    if (stackSet.has(obj)) { // it cyclic! Print the object and its locations.
      var oldindex = stack.indexOf(obj);
      var l1 = keys.join('.') + '.' + key;
      var l2 = keys.slice(0, oldindex + 1).join('.');
      console.log('CIRCULAR: ' + l1 + ' = ' + l2 + ' = ' + obj);
      console.log(obj);
      detected = true;
      return;
    }

    keys.push(key);
    stack.push(obj);
    stackSet.add(obj);
    for (var k in obj) { //dive on the object children
      if (Object.prototype.hasOwnProperty.call(obj, k)) { detect(obj[k], k); }
    }

    keys.pop();
    stack.pop();
    stackSet.delete(obj);
    return;
  }

  detect(obj, 'obj');
  return detected;
}

Вот несколько очень простых тестов:

var root = {}
var leaf = {'isleaf':true};
var cycle2 = {l:leaf};
var cycle1 = {c2: cycle2, l:leaf};
cycle2.c1 = cycle1
root.leaf = leaf

isCyclic(cycle1); // returns true, logs "CIRCULAR: obj.c2.c1 = obj"
isCyclic(cycle2); // returns true, logs "CIRCULAR: obj.c1.c2 = obj"
isCyclic(leaf); // returns false
isCyclic(root); // returns false

Ответ 3

CircularReferenceDetector

Вот мой класс CircularReferenceDetector, который выводит всю информацию о стеке свойств, где фактически указано значение с круговой ссылкой, а также показывает, где находятся ссылки на виновники.

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

Он выводит значение, связанное с круговыми ссылками, но все ссылки на себя заменены на "[Circular object --- fix me]".

Применение:
CircularReferenceDetector.detectCircularReferences(value);

Примечание: Удалите операторы Logger. *, Если вы не хотите использовать какой-либо журнал или не имеете доступ к журналу.

Техническое объяснение:
Рекурсивная функция проходит через все свойства объекта и проверяет, удастся ли им выполнить JSON.stringify или нет. Если это не удастся (круговая ссылка), тогда он проверяет, удастся ли это путем замены значения самой константной строкой. Это означало бы, что, если ему удастся использовать этот заменитель, это значение является значением, связанным с круговым значением. Если это не так, он рекурсивно проходит через все свойства этого объекта.

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

Typescript

import {Logger} from "../Logger";

export class CircularReferenceDetector {

    static detectCircularReferences(toBeStringifiedValue: any, serializationKeyStack: string[] = []) {
        Object.keys(toBeStringifiedValue).forEach(key => {
            var value = toBeStringifiedValue[key];

            var serializationKeyStackWithNewKey = serializationKeyStack.slice();
            serializationKeyStackWithNewKey.push(key);
            try {
                JSON.stringify(value);
                Logger.debug(`path "${Util.joinStrings(serializationKeyStack)}" is ok`);
            } catch (error) {
                Logger.debug(`path "${Util.joinStrings(serializationKeyStack)}" JSON.stringify results in error: ${error}`);

                var isCircularValue:boolean;
                var circularExcludingStringifyResult:string = "";
                try {
                    circularExcludingStringifyResult = JSON.stringify(value, CircularReferenceDetector.replaceRootStringifyReplacer(value), 2);
                    isCircularValue = true;
                } catch (error) {
                    Logger.debug(`path "${Util.joinStrings(serializationKeyStack)}" is not the circular source`);
                    CircularReferenceDetector.detectCircularReferences(value, serializationKeyStackWithNewKey);
                    isCircularValue = false;
                }
                if (isCircularValue) {
                    throw new Error(`Circular reference detected:\nCircularly referenced value is value under path "${Util.joinStrings(serializationKeyStackWithNewKey)}" of the given root object\n`+
                        `Calling stringify on this value but replacing itself with [Circular object --- fix me] ( <-- search for this string) results in:\n${circularExcludingStringifyResult}\n`);
                }
            }
        });
    }

    private static replaceRootStringifyReplacer(toBeStringifiedValue: any): any {
        var serializedObjectCounter = 0;

        return function (key: any, value: any) {
            if (serializedObjectCounter !== 0 && typeof(toBeStringifiedValue) === 'object' && toBeStringifiedValue === value) {
                Logger.error(`object serialization with key ${key} has circular reference to being stringified object`);
                return '[Circular object --- fix me]';
            }

            serializedObjectCounter++;

            return value;
        }
    }
}

export class Util {

    static joinStrings(arr: string[], separator: string = ":") {
        if (arr.length === 0) return "";
        return arr.reduce((v1, v2) => `${v1}${separator}${v2}`);
    }

}

Ответ 4

Это исправление для ответов @Trey Mack и @Freddie Nfbnm в typeof obj != 'object'. Вместо этого он должен проверить, является ли значение obj не экземпляром объекта, так что он также может работать при проверке значений с знакомности объекта (например, функции и символы (символы не являются экземпляром объекта, но все же обратился, кстати.)).

Я отправляю это как ответ, так как я еще не могу комментировать эту учетную запись StackExchange.

PS: не стесняйтесь просить меня удалить этот ответ.

function isCyclic(obj) {
  var keys = [];
  var stack = [];
  var stackSet = new Set();
  var detected = false;

  function detect(obj, key) {
    if (!(obj instanceof Object)) { return; } // Now works with other
                                              // kinds of object.

    if (stackSet.has(obj)) { // it cyclic! Print the object and its locations.
      var oldindex = stack.indexOf(obj);
      var l1 = keys.join('.') + '.' + key;
      var l2 = keys.slice(0, oldindex + 1).join('.');
      console.log('CIRCULAR: ' + l1 + ' = ' + l2 + ' = ' + obj);
      console.log(obj);
      detected = true;
      return;
    }

    keys.push(key);
    stack.push(obj);
    stackSet.add(obj);
    for (var k in obj) { //dive on the object children
      if (obj.hasOwnProperty(k)) { detect(obj[k], k); }
    }

    keys.pop();
    stack.pop();
    stackSet.delete(obj);
    return;
  }

  detect(obj, 'obj');
  return detected;
}

Ответ 5

Вы также можете использовать JSON.stringify с try/catch

function hasCircularDependency(obj)
{
    try
    {
        JSON.stringify(obj);
    }
    catch(e)
    {
        return e.includes("Converting circular structure to JSON"); 
    }
    return false;
}

демонстрация

function hasCircularDependency(obj) {
  try {
    JSON.stringify(obj);
  } catch (e) {
    return String(e).includes("Converting circular structure to JSON");
  }
  return false;
}

var a = {b:{c:{d:""}}};
console.log(hasCircularDependency(a));
a.b.c.d = a;
console.log(hasCircularDependency(a));

Ответ 6

Вот версия Node ES6, смешанная с ответами @Aaron V и @user4976005, она исправляет проблему с вызовом hasOwnProperty:

const isCyclic = (obj => {
  const keys = []
  const stack = []
  const stackSet = new Set()
  let detected = false

  const detect = ((object, key) => {
    if (!(object instanceof Object))
      return

    if (stackSet.has(object)) { // it cyclic! Print the object and its locations.
      const oldindex = stack.indexOf(object)
      const l1 = '${keys.join('.')}.${key}'
      const l2 = keys.slice(0, oldindex + 1).join('.')
      console.log('CIRCULAR: ${l1} = ${l2} = ${object}')
      console.log(object)
      detected = true
      return
    }

    keys.push(key)
    stack.push(object)
    stackSet.add(object)
    Object.keys(object).forEach(k => { // dive on the object children
      if (k && Object.prototype.hasOwnProperty.call(object, k))
        detect(object[k], k)
    })

    keys.pop()
    stack.pop()
    stackSet.delete(object)
  })

  detect(obj, 'obj')
  return detected
})

Ответ 7

Я только что сделал это. Это может быть грязно, но все равно работает...: P

function dump(orig){
  var inspectedObjects = [];
  console.log('== DUMP ==');
  (function _dump(o,t){
    console.log(t+' Type '+(typeof o));
    for(var i in o){
      if(o[i] === orig){
        console.log(t+' '+i+': [recursive]'); 
        continue;
      }
      var ind = 1+inspectedObjects.indexOf(o[i]);
      if(ind>0) console.log(t+' '+i+':  [already inspected ('+ind+')]');
      else{
        console.log(t+' '+i+': ('+inspectedObjects.push(o[i])+')');
        _dump(o[i],t+'>>');
      }
    }
  }(orig,'>'));
}

Тогда

var a = [1,2,3], b = [a,4,5,6], c = {'x':a,'y':b};
a.push(c); dump(c);

Говорит

== DUMP ==
> Type object
> x: (1)
>>> Type object
>>> 0: (2)
>>>>> Type number
>>> 1: (3)
>>>>> Type number
>>> 2: (4)
>>>>> Type number
>>> 3: [recursive]
> y: (5)
>>> Type object
>>> 0:  [already inspected (1)]
>>> 1: (6)
>>>>> Type number
>>> 2: (7)
>>>>> Type number
>>> 3: (8)
>>>>> Type number

Это говорит о том, что c.x [3] равен c, а c.x = c.y [0].

Или небольшое редактирование этой функции может рассказать вам, что вам нужно...

function findRecursive(orig){
  var inspectedObjects = [];
  (function _find(o,s){
    for(var i in o){
      if(o[i] === orig){
        console.log('Found: obj.'+s.join('.')+'.'+i); 
        return;
      }
      if(inspectedObjects.indexOf(o[i])>=0) continue;
      else{
        inspectedObjects.push(o[i]);
        s.push(i); _find(o[i],s); s.pop(i);
      }
    }
  }(orig,[]));
}

Ответ 8

Вот @Ответ Тома, адаптированный для node:

const {logger} = require("../logger")
// Or: const logger = {debug: (...args) => console.log.call(console.log, args) }

const joinStrings = (arr, separator) => {
  if (arr.length === 0) return "";
  return arr.reduce((v1, v2) => `${v1}${separator}${v2}`);
}

exports.CircularReferenceDetector = class CircularReferenceDetector {

  detectCircularReferences(toBeStringifiedValue, serializationKeyStack = []) {
    Object.keys(toBeStringifiedValue).forEach(key => {
      let value = toBeStringifiedValue[key];

      let serializationKeyStackWithNewKey = serializationKeyStack.slice();
      serializationKeyStackWithNewKey.push(key);
      try {
        JSON.stringify(value);
        logger.debug(`path "${joinStrings(serializationKeyStack)}" is ok`);
      } catch (error) {
        logger.debug(`path "${joinStrings(serializationKeyStack)}" JSON.stringify results in error: ${error}`);

        let isCircularValue;
        let circularExcludingStringifyResult = "";
        try {
          circularExcludingStringifyResult = JSON.stringify(value, this.replaceRootStringifyReplacer(value), 2);
          isCircularValue = true;
        } catch (error) {
          logger.debug(`path "${joinStrings(serializationKeyStack)}" is not the circular source`);
          this.detectCircularReferences(value, serializationKeyStackWithNewKey);
          isCircularValue = false;
        }
        if (isCircularValue) {
          throw new Error(`Circular reference detected:\nCircularly referenced value is value under path "${joinStrings(serializationKeyStackWithNewKey)}" of the given root object\n`+
              `Calling stringify on this value but replacing itself with [Circular object --- fix me] ( <-- search for this string) results in:\n${circularExcludingStringifyResult}\n`);
        }
      }
    });
  }

  replaceRootStringifyReplacer(toBeStringifiedValue) {
    let serializedObjectCounter = 0;

    return function (key, value) {
      if (serializedObjectCounter !== 0 && typeof(toBeStringifiedValue) === 'object' && toBeStringifiedValue === value) {
        logger.error(`object serialization with key ${key} has circular reference to being stringified object`);
        return '[Circular object --- fix me]';
      }

      serializedObjectCounter++;

      return value;
    }
  }
}

Ответ 9

Я преобразовал ответ Freddie Nfbnm в TypeScript:

export class JsonUtil {

    static isCyclic(json) {
        const keys = [];
        const stack = [];
        const stackSet = new Set();
        let detected = false;

        function detect(obj, key) {
            if (typeof obj !== 'object') {
                return;
            }

            if (stackSet.has(obj)) { // it cyclic! Print the object and its locations.
                const oldIndex = stack.indexOf(obj);
                const l1 = keys.join('.') + '.' + key;
                const l2 = keys.slice(0, oldIndex + 1).join('.');
                console.log('CIRCULAR: ' + l1 + ' = ' + l2 + ' = ' + obj);
                console.log(obj);
                detected = true;
                return;
            }

            keys.push(key);
            stack.push(obj);
            stackSet.add(obj);
            for (const k in obj) { // dive on the object children
                if (obj.hasOwnProperty(k)) {
                    detect(obj[k], k);
                }
            }

            keys.pop();
            stack.pop();
            stackSet.delete(obj);
            return;
        }

        detect(json, 'obj');
        return detected;
    }

}

Ответ 11

Здесь много ответов, но я решил добавить свое решение в микс. Это похоже на ответ @Trey Mack, но это решение занимает O (n ^ 2). Эта версия использует WeakMap вместо массива, улучшая время до O (n).

function isCyclic(object) {
   const seenObjects = new WeakMap(); // use to keep track of which objects have been seen.

   function detectCycle(obj) {
      // If 'obj' is an actual object (i.e., has the form of '{}'), check
      // if it been seen already.
      if (Object.prototype.toString.call(obj) == '[object Object]') {

         if (seenObjects.has(obj)) {
            return true;
         }

         // If 'obj' hasn't been seen, add it to 'seenObjects'.
         // Since 'obj' is used as a key, the value of 'seenObjects[obj]'
         // is irrelevent and can be set as literally anything you want. I 
         // just went with 'undefined'.
         seenObjects.set(obj, undefined);

         // Recurse through the object, looking for more circular references.
         for (var key in obj) {
            if (detectCycle(obj[key])) {
               return true;
            }
         }

      // If 'obj' is an array, check if any of it elements are
      // an object that has been seen already.
      } else if (Array.isArray(obj)) {
         for (var i in obj) {
            if (detectCycle(obj[i])) {
               return true;
            }
         }
      }

      return false;
   }

   return detectCycle(object);
}

И вот как это выглядит в действии.

> var foo = {grault: {}};
> detectCycle(foo);
false
> foo.grault = foo;
> detectCycle(foo);
true
> var bar = {};
> detectCycle(bar);
false
> bar.plugh = [];
> bar.plugh.push(bar);
> detectCycle(bar);
true

Ответ 12

Попробуйте использовать console.log() в браузере Chrome/firefox, чтобы определить, где возникла проблема.

В Firefox с использованием Firebug-плагина вы можете отлаживать ваш javascript по строкам.

Обновление:

См. ниже пример круговой справки и который был обработан: -

// JSON.stringify, avoid TypeError: Converting circular structure to JSON
// Demo: Circular reference
var o = {};
o.o = o;

var cache = [];
JSON.stringify(o, function(key, value) {
    if (typeof value === 'object' && value !== null) {
        if (cache.indexOf(value) !== -1) {
            // Circular reference found, discard key
            alert("Circular reference found, discard key");
            return;
        }
        alert("value = '" + value + "'");
        // Store value in our collection
        cache.push(value);
    }
    return value;
});
cache = null; // Enable garbage collection

var a = {b:1};
var o = {};
o.one = a;
o.two = a;
// one and two point to the same object, but two is discarded:
JSON.stringify(o);

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

var replacement = {"b":undefined};

alert("Result : " + JSON.stringify(obj,replacement));

См. пример LIVE DEMO