在JavaScript中检测和修复循环引用

Mad*_*bæk 56 javascript

鉴于我在一个大的JavaScript对象中有一个循环引用

我试试 JSON.stringify(problematicObject)

而浏览器抛出

"TypeError:将循环结构转换为JSON"

(预计)

那么我想找到这个循环引用的原因,最好使用Chrome开发人员工具?这可能吗?如何在大型对象中查找和修复循环引用?

Tre*_*ack 51

来自http://blog.vjeux.com/2011/javascript/cyclic-object-detection.html.添加一行以检测循环的位置.将其粘贴到Chrome开发工具中:

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);
}
Run Code Online (Sandbox Code Playgroud)

这是测试:

> a = {}
> b = {}
> a.b = b; b.a = a;
> isCyclic(a)
  Object {a: Object}
   "cycle at a"
  Object {b: Object}
   "cycle at b"
  true
Run Code Online (Sandbox Code Playgroud)

  • 仅仅因为参考被多次使用并不一定意味着它是循环的.`var x = {}; JSON.stringify([x,x])`很好......而`var x = {}; xx = x; JSON.stringify(x);`不是. (22认同)
  • 这个误报为null并且不是递归的 (3认同)
  • 这不是循环`z = {};x = {x1: z, x2: z};` 但您的代码 `isCyclic(z)` 返回 `true` (3认同)

Aar*_*n V 42

当我发现这个问题时,@ tmack的答案绝对是我所寻找的!

不幸的是,它会返回许多误报 - 如果在JSON中复制了一个对象,它将返回true,这圆度不同.圆度意味着一个对象是它自己的孩子,例如

obj.key1.key2.[...].keyX === obj
Run Code Online (Sandbox Code Playgroud)

我修改了原来的答案,这对我有用:

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's 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's 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;
}
Run Code Online (Sandbox Code Playgroud)

以下是一些非常简单的测试:

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
Run Code Online (Sandbox Code Playgroud)

  • `if(typeof obj!='object'){return; }`应该是`if(obj && typeof obj!='object'){return; 因为`typeof null =="object"`. (3认同)
  • 添加记忆以使这个 O(N) 留给读者作为练习:) (2认同)

Tho*_*mas 7

循环参考检测器

这是我的CircularReferenceDetector类,它输出循环引用值实际所在的所有属性堆栈信息,还显示了罪魁祸首引用的位置。

这对于巨大的结构特别有用,其中关键是哪个值是危害的来源并不明显。

它输出字符串化的循环引用值,但所有对自身的引用都替换为“[Circular object --- fix me]”。

用法:
CircularReferenceDetector.detectCircularReferences(value);

注意: 如果您不想使用任何日志记录或没有可用的记录器,请删除 Logger.* 语句。

技术说明:
递归函数遍历对象的所有属性并测试 JSON.stringify 是否成功。如果它不成功(循环引用),则它通过用某个常量字符串替换 value 本身来测试它是否成功。这意味着如果它成功使用这个替换器,这个值就是被循环引用的值。如果不是,它将递归遍历该对象的所有属性。

同时,它还跟踪属性堆栈,为您提供罪魁祸首值所在的信息。

打字稿

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}`);
    }

}
Run Code Online (Sandbox Code Playgroud)

从 TypeScript 编译的 JavaScript

"use strict";
const Logger_1 = require("../Logger");
class CircularReferenceDetector {
    static detectCircularReferences(toBeStringifiedValue, serializationKeyStack = []) {
        Object.keys(toBeStringifiedValue).forEach(key => {
            var value = toBeStringifiedValue[key];
            var serializationKeyStackWithNewKey = serializationKeyStack.slice();
            serializationKeyStackWithNewKey.push(key);
            try {
                JSON.stringify(value);
                Logger_1.Logger.debug(`path "${Util.joinStrings(serializationKeyStack)}" is ok`);
            }
            catch (error) {
                Logger_1.Logger.debug(`path "${Util.joinStrings(serializationKeyStack)}" JSON.stringify results in error: ${error}`);
                var isCircularValue;
                var circularExcludingStringifyResult = "";
                try {
                    circularExcludingStringifyResult = JSON.stringify(value, CircularReferenceDetector.replaceRootStringifyReplacer(value), 2);
                    isCircularValue = true;
                }
                catch (error) {
                    Logger_1.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`);
                }
            }
        });
    }
    static replaceRootStringifyReplacer(toBeStringifiedValue) {
        var serializedObjectCounter = 0;
        return function (key, value) {
            if (serializedObjectCounter !== 0 && typeof (toBeStringifiedValue) === 'object' && toBeStringifiedValue === value) {
                Logger_1.Logger.error(`object serialization with key ${key} has circular reference to being stringified object`);
                return '[Circular object --- fix me]';
            }
            serializedObjectCounter++;
            return value;
        };
    }
}
exports.CircularReferenceDetector = CircularReferenceDetector;
class Util {
    static joinStrings(arr, separator = ":") {
        if (arr.length === 0)
            return "";
        return arr.reduce((v1, v2) => `${v1}${separator}${v2}`);
    }
}
exports.Util = Util;
Run Code Online (Sandbox Code Playgroud)

  • 应该将此发布到 npm (2认同)

gur*_*372 7

您也可以JSON.stringifytry/catch 一起使用

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

演示

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

  • 你是用问题本身来回答问题吗? (3认同)

dar*_*nge 6

这里有很多答案,但我想我应该将我的解决方案添加到其中。它类似于@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's 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's 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);
}
Run Code Online (Sandbox Code Playgroud)

这就是它实际的样子。

> 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
Run Code Online (Sandbox Code Playgroud)


小智 5

这是针对条件上的@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's 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's children
      if (obj.hasOwnProperty(k)) { detect(obj[k], k); }
    }

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

  detect(obj, 'obj');
  return detected;
}
Run Code Online (Sandbox Code Playgroud)


dku*_*zaj 5

这是一个从@Aaron V@user4976005的答案混合而成的 Node ES6 版本,它解决了调用 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's 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's 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
})
Run Code Online (Sandbox Code Playgroud)


Mat*_*150 5

以下是 MDNJSON.stringify()在循环对象上使用时检测和修复循环引用的方法:https : //developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/Cyclic_object_value

在如下所示的圆形结构中

var circularReference = {otherData: 123};
circularReference.myself = circularReference;
Run Code Online (Sandbox Code Playgroud)

JSON.stringify() 将失败:

JSON.stringify(circularReference);
// TypeError: cyclic object value
Run Code Online (Sandbox Code Playgroud)

要序列化循环引用,您可以使用支持它们的库(例如cycle.js)或自己实现解决方案,这将需要通过可序列化值查找和替换(或删除)循环引用。

下面的代码片段说明了如何使用的替换器参数查找和过滤(从而导致数据丢失)循环引用:JSON.stringify()

const getCircularReplacer = () => {
      const seen = new WeakSet();
      return (key, value) => {
        if (typeof value === "object" && value !== null) {
          if (seen.has(value)) {
            return;
          }
          seen.add(value);
        }
        return value;
      };
    };

JSON.stringify(circularReference, getCircularReplacer());
// {"otherData":123}
Run Code Online (Sandbox Code Playgroud)

  • 这是最好的答案。每个人都想编写自己的本土代码。另一方面,MDN是权威的。如果这段代码太具体(它应该放在 JSON.stringify 中),那么请查看他们引用的代码,cycle.js,该代码是由知道他们在做什么的人编写的。 (3认同)
  • 美丽的。他妈的把这个答案粘到首页上。 (3认同)

Siv*_*ran -1

尝试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));
Run Code Online (Sandbox Code Playgroud)

请参阅示例现场演示

  • 您的意思是“手动”搜索参考?这更像是最后的手段,而不是一个好的解决方案。 (5认同)
  • 仅仅因为引用被多次使用并不一定意味着它是循环的。`var x = {}; JSON.stringify([x,x])` 很好...而 `var x = {}; xx=x;JSON.stringify(x);` 不是。 (4认同)
  • *“您可以逐行调试您的 javascript”* - 内置的 `JSON.stringify()` 不是在 JavaScript 中实现的,而是在本机代码中实现的。您无法使用 Firebug 对其进行调试。 (3认同)