Detecting and fixing circular references in JavaScript

后端 未结 15 805
庸人自扰
庸人自扰 2020-11-28 23:29

Given I have a circular reference in a large JavaScript object

And I try JSON.stringify(problematicObject)

And the browser throws

相关标签:
15条回答
  • 2020-11-28 23:58

    There's a lot of answers here, but I thought I'd add my solution to the mix. It's similar to @Trey Mack's answer, but that solution takes O(n^2). This version uses WeakMap instead of an array, improving the time to 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);
    }
    

    And this is what it looks like in action.

    > 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
    
    0 讨论(0)
  • 2020-11-28 23:59

    I just made this. It may be dirty, but works anyway... :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,'>'));
    }
    

    Then

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

    Says

    == 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
    

    This tells that c.x[3] is equal to c, and c.x = c.y[0].

    Or, a little edit to this function can tell you what you need...

    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,[]));
    }
    
    0 讨论(0)
  • 2020-11-29 00:00

    Here is a Node ES6 version mixed from the answers from @Aaron V and @user4976005, it fixes the problem with the call to 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
    })
    
    0 讨论(0)
  • 2020-11-29 00:00

    You can also use JSON.stringify with try/catch

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

    Demo

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

    0 讨论(0)
  • 2020-11-29 00:06

    Try using console.log() on the chrome/firefox browser to identify where the issue encountered.

    On Firefox using Firebug plugin, you can debug your javascript line by line.

    Update:

    Refer below example of circular reference issue and which has been handled:-

    // 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));
    

    Refer example LIVE DEMO

    0 讨论(0)
  • 2020-11-29 00:09

    Just to throw my version into the mix... below is a remix of @dkurzaj 's code (which is itself a remix of @Aaron V 's, @user4976005 's, @Trey Mack 's and finally @Freddie Nfbnm 's [removed?] code) plus @darksinge 's WeakMap idea. So... this thread's Megamix, I guess :)

    In my version, a report (rather than console.log'ed entries) is optionally returned as an array of objects. If a report is not required, testing stops on the first sighting of a circular reference (a'la @darksinge 's code).

    Further, hasOwnProperty has been removed as Object.keys returns only hasOwnProperty properties (see: 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);
    }
    
    0 讨论(0)
提交回复
热议问题