【问题标题】:Detecting and fixing circular references in JavaScript检测和修复 JavaScript 中的循环引用
【发布时间】:2013-02-04 09:38:20
【问题描述】:

鉴于我在大型 JavaScript 对象中有循环引用

我试试JSON.stringify(problematicObject)

浏览器抛出

“TypeError:将循环结构转换为 JSON”

(这是预期的)

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

【问题讨论】:

  • 你能发布problematicObject是什么吗?
  • @jbabey - JSON.stringify(window); 是导致该错误的一种情况。
  • 它是原生对象(例如,通过{} 创建的,或原生函数之一),还是宿主对象(例如,DOM 元素)?

标签: javascript


【解决方案1】:

大多数其他答案仅显示如何检测对象树具有循环引用——它们没有告诉您如何修复这些循环引用(即将循环引用值替换为 undefined)。

下面是我用undefined替换所有循环引用的函数:

export const specialTypeHandlers_default = [
    // Set and Map are included by default, since JSON.stringify tries (and fails) to serialize them by default
    {type: Set, keys: a=>a.keys(), get: (a, key)=>key, delete: (a, key)=>a.delete(key)},
    {type: Map, keys: a=>a.keys(), get: (a, key)=>a.get(key), delete: (a, key)=>a.set(key, undefined)},
];
export function RemoveCircularLinks(node, specialTypeHandlers = specialTypeHandlers_default, nodeStack_set = new Set()) {
    nodeStack_set.add(node);

    const specialHandler = specialTypeHandlers.find(a=>node instanceof a.type);
    for (const key of specialHandler ? specialHandler.keys(node) : Object.keys(node)) {
        const value = specialHandler ? specialHandler.get(node, key) : node[key];
        // if the value is already part of visited-stack, delete the value (and don't tunnel into it)
        if (nodeStack_set.has(value)) {
            if (specialHandler) specialHandler.delete(node, key);
            else node[key] = undefined;
        }
        // else, tunnel into it, looking for circular-links at deeper levels
        else if (typeof value == "object" && value != null) {
            RemoveCircularLinks(value, specialTypeHandlers, nodeStack_set);
        }
    }

    nodeStack_set.delete(node);
}

特别是与JSON.stringify一起使用,只需在字符串化之前调用上面的函数(注意它确实改变了传入的对象):

const objTree = {normalProp: true};
objTree.selfReference = objTree;
RemoveCircularLinks(objTree); // without this line, the JSON.stringify call errors
console.log(JSON.stringify(objTree));

【讨论】:

    【解决方案2】:

    循环参考检测器

    这是我的 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}`);
        }
    
    }
    

    从 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;
    

    【讨论】:

    • 应该将它发布到 npm
    • 这会导致无限循环,将数千个条目记录到控制台,其他答案之一也会导致无限循环。但是,另一个答案中 isCyclic 的原始实现并没有导致无限循环。
    • 谢谢你的好例子,但是如果你不知道循环结构的对象在哪里呢?因为在我的情况下,我几个月都无法捕捉到它,我的应用程序有很多第三方库,其中许多使用 JSON.stringify。你有什么推荐的?
    【解决方案3】:

    您也可以使用符号 - 由于这种方法,您不必改变原始对象的属性,除了添加符号来标记访问的节点。

    它更干净,应该比收集节点属性和与对象比较更快。如果您不想序列化大的嵌套值,它还具有可选的深度限制:

    // Symbol used to mark already visited nodes - helps with circular dependencies
    const visitedMark = Symbol('VISITED_MARK');
    
    const MAX_CLEANUP_DEPTH = 10;
    
    function removeCirculars(obj, depth = 0) {
      if (!obj) {
        return obj;
      }
    
      // Skip condition - either object is falsy, was visited or we go too deep
      const shouldSkip = !obj || obj[visitedMark] || depth > MAX_CLEANUP_DEPTH;
    
      // Copy object (we copy properties from it and mark visited nodes)
      const originalObj = obj;
      let result = {};
    
      Object.keys(originalObj).forEach((entry) => {
        const val = originalObj[entry];
    
        if (!shouldSkip) {
          if (typeof val === 'object') { // Value is an object - run object sanitizer
            originalObj[visitedMark] = true; // Mark current node as "seen" - will stop from going deeper into circulars
            const nextDepth = depth + 1;
            result[entry] = removeCirculars(val, nextDepth);
          } else {
            result[entry] = val;
          }
        } else {
          result = 'CIRCULAR';
        }
      });
    
      return result;
    }
    

    这将导致对象的所有循环依赖项被剥离,并且不会比给定的MAX_CLEANUP_DEPTH 更深。

    只要您不对对象执行任何元编程操作,使用符号是安全的 - 它们是透明的且不可枚举,因此 - 它们不会显示在对象的任何标准操作中。

    此外,如果您需要对其执行任何其他操作,返回一个新的、已清理的对象具有不会改变原始对象的优点。

    如果您不想标记CIRCULAR,您可以稍微修改一下代码,从而在实际执行操作之前跳过对象(在循环内):

     originalObj[visitedMark] = true; // Mark current node as "seen" - will stop from going deeper into circulars
     const val = originalObj[entry];
    
     // Skip condition - either object is falsy, was visited or we go too deep
     const shouldSkip = val[visitedMark] || depth > MAX_SANITIZATION_DEPTH;
    
     if (!shouldSkip) {
       if (typeof val === 'object') { // Value is an object - run object sanitizer
        const nextDepth = depth + 1;
        result[entry] = removeCirculars(val, nextDepth);
      } else {
        result[entry] = val;
      }
     }
    

    【讨论】:

    【解决方案4】:

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

    像下面这样的循环结构

    var circularReference = {otherData: 123};
    circularReference.myself = circularReference;
    

    JSON.stringify() 将失败:

    JSON.stringify(circularReference);
    // TypeError: cyclic object value
    

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

    下面的 sn-p 说明了如何使用 the replacer parameter of 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}
    

    【讨论】:

    • 这是最好的答案。每个人都想编写自己的本土代码。另一方面,MDN 是权威的。如果这段代码太具体(它应该放在 JSON.stringify 中),那么查看他们引用的代码 cycle.js,它是由知道他们在做什么的人编写的。
    【解决方案5】:

    只是为了把我的版本混在一起...下面是 @dkurzaj 's code 的混音(它本身是 @Aaron V 的混音,@user4976005 的混音,@Trey Mack 的混音,最后是 @Freddie Nfbnm的 [已删除?] 代码)加上 @darksinge 's WeakMap idea。所以……我猜这个帖子是 Megamix :)

    在我的版本中,可以选择将报告(而不是console.log'ed 条目)作为对象数组返回。如果不需要报告,则在第一次看到循环引用(a'la @darksinge 的代码)时停止测试。

    此外,hasOwnProperty 已被删除,因为 Object.keys 仅返回 hasOwnProperty 属性(请参阅:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/keys)。

    function isCyclic(x, bReturnReport) {
        var a_sKeys = [],
            a_oStack = [],
            wm_oSeenObjects = new WeakMap(), //# see: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakMap
            oReturnVal = {
                found: false,
                report: []
            }
        ;
    
        //# Setup the recursive logic to locate any circular references while kicking off the initial call
        (function doIsCyclic(oTarget, sKey) {
            var a_sTargetKeys, sCurrentKey, i;
    
            //# If we've seen this oTarget before, flip our .found to true
            if (wm_oSeenObjects.has(oTarget)) {
                oReturnVal.found = true;
    
                //# If we are to bReturnReport, add the entries into our .report
                if (bReturnReport) {
                    oReturnVal.report.push({
                        instance: oTarget,
                        source: a_sKeys.slice(0, a_oStack.indexOf(oTarget) + 1).join('.'),
                        duplicate: a_sKeys.join('.') + "." + sKey
                    });
                }
            }
            //# Else if oTarget is an instanceof Object, determine the a_sTargetKeys and .set our oTarget into the wm_oSeenObjects
            else if (oTarget instanceof Object) {
                a_sTargetKeys = Object.keys(oTarget);
                wm_oSeenObjects.set(oTarget /*, undefined*/);
    
                //# If we are to bReturnReport, .push the  current level's/call's items onto our stacks
                if (bReturnReport) {
                    if (sKey) { a_sKeys.push(sKey) };
                    a_oStack.push(oTarget);
                }
    
                //# Traverse the a_sTargetKeys, pulling each into sCurrentKey as we go
                //#     NOTE: If you want all properties, even non-enumerables, see Object.getOwnPropertyNames() so there is no need to call .hasOwnProperty (per: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/keys)
                for (i = 0; i < a_sTargetKeys.length; i++) {
                    sCurrentKey = a_sTargetKeys[i];
    
                    //# If we've already .found a circular reference and we're not bReturnReport, fall from the loop
                    if (oReturnVal.found && !bReturnReport) {
                        break;
                    }
                    //# Else if the sCurrentKey is an instanceof Object, recurse to test
                    else if (oTarget[sCurrentKey] instanceof Object) {
                        doIsCyclic(oTarget[sCurrentKey], sCurrentKey);
                    }
                }
    
                //# .delete our oTarget into the wm_oSeenObjects
                wm_oSeenObjects.delete(oTarget);
    
                //# If we are to bReturnReport, .pop the current level's/call's items off our stacks
                if (bReturnReport) {
                    if (sKey) { a_sKeys.pop() };
                    a_oStack.pop();
                }
            }
        }(x, '')); //# doIsCyclic
    
        return (bReturnReport ? oReturnVal.report : oReturnVal.found);
    }
    

    【讨论】:

      【解决方案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);
      }
      

      这就是它在行动中的样子。

      > 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
      

      【讨论】:

        【解决方案7】:

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

        不幸的是,它会返回许多误报 - 如果在 JSON 中复制对象,它会返回 true,这与循环性不同。循环性意味着一个对象是它自己的孩子,例如

        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'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;
        }
        

        这里有一些非常简单的测试:

        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
        

        【讨论】:

        • 添加记忆以使这个 O(N) 留给读者作为练习:)
        • 我添加了这个答案的 TypeScript 版本作为新答案。见下文。
        • if (typeof obj != 'object') { return; } 应该是 if (obj &amp;&amp; typeof obj != 'object') { return; } 因为typeof null == "object"
        • @Willwsharp - 已经有一段时间了,但我相信这个想法是绝对有可能多次遇到同一个物体,即使在非圆形结构中也是如此。因此,在最坏的情况下,我在这里编写的代码会一遍又一遍地处理相同的对象,这使得这不必要地 O(N^2)。您可以使用 memoization 来存储每个对象的子对象并防止重新处理。
        • @AaronV 哦,太棒了!我在这方面做了更多工作,得出了同样的结论,很高兴听到我走在正确的道路上;谢谢!
        【解决方案8】:

        这是由@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
        })
        

        【讨论】:

          【解决方案9】:

          你也可以将JSON.stringifytry/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));

          【讨论】:

          • 你是用问题本身来回答问题吗?
          • OP 已经知道这是导致错误的循环引用,他在问题I want to find the **cause** of this circular reference 中说(强调我的)。所以你的回答根本不回答OP的问题。
          • @hudidit 我想将您的评论标记为有趣。此外,对于更多但同样不重要的信息,我无法将我自己的评论标记为任何内容。
          【解决方案10】:

          我将 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's 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's children
                          if (obj.hasOwnProperty(k)) {
                              detect(obj[k], k);
                          }
                      }
          
                      keys.pop();
                      stack.pop();
                      stackSet.delete(obj);
                      return;
                  }
          
                  detect(json, 'obj');
                  return detected;
              }
          
          }
          

          【讨论】:

            【解决方案11】:

            这里是@Thomas's answer 适用于节点:

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

            【讨论】:

              【解决方案12】:

              这是对typeof obj != 'object' 条件下@Trey Mack@Freddie Nfbnm 答案的修复。相反,它应该测试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;
              }
              

              【讨论】:

                【解决方案13】:

                来自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);
                }
                

                这是测试:

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

                【讨论】:

                • 不要忘记将根 JSON 对象本身添加到 seenObjects。应该是var seenObjects = [obj];
                • @AaditMShah,因为在输入对象上调用了detect,这将导致它立即退出,声称从输入对象直接到自身存在一个循环。
                • 这个误报为 null 并且不是递归的
                • 一个引用被多次使用并不一定意味着它是循环的。 var x = {}; JSON.stringify([x,x]) 很好......而var x = {}; x.x = x; JSON.stringify(x); 不是。
                • 由于某种原因,当我使用此解决方案时,它给了我一个错误:obj.hasOwnProperty is not a function。这是在浏览器加载的脚本中运行的。
                【解决方案14】:

                尝试在 chrome/firefox 浏览器上使用 console.log() 以确定问题所在。

                在使用 Firebug 插件的 Firefox 上,您可以逐行调试您的 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

                【讨论】:

                • 您的意思是手动搜索参考资料?这与其说是一个好的解决方案,不如说是最后的手段。
                • “你可以逐行调试你的 javascript” - 内置 JSON.stringify() 不是用 JavaScript 实现的,而是用原生代码实现的。你不能用 Firebug 调试它。
                • 有很多库在 JavaScript 中为旧版浏览器提供 stringify() 函数。也许你可以使用其中之一?这可能会在它解析问题引用的确切点中断。
                • 引用被多次使用并不一定意味着它是循环的。 var x = {}; JSON.stringify([x,x]) 很好......而var x = {}; x.x = x; JSON.stringify(x); 不是。
                【解决方案15】:

                我刚做了这个。它可能很脏,但无论如何都可以使用...: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,[]));
                }
                

                【讨论】:

                • inspectedObjects 数组应该包含原始 JSON 对象本身(即它应该是 var inspectedObjects = [orig];)。
                • 我故意分开检查原始对象(if(o[i] === orig))和其他对象(inspectedObjects.indexOf(o[i]))。
                • 引用被多次使用并不一定意味着它是循环的。 var x = {}; JSON.stringify([x,x]) 很好......而var x = {}; x.x = x; JSON.stringify(x); 不是。
                猜你喜欢
                • 2014-02-26
                • 2018-06-03
                • 2011-11-24
                • 2016-12-15
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 2022-01-20
                • 2016-04-17
                相关资源
                最近更新 更多