Detecting and fixing circular references in JavaScript

后端 未结 15 807
庸人自扰
庸人自扰 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-29 00:20

    Pulled from http://blog.vjeux.com/2011/javascript/cyclic-object-detection.html. One line added to detect where the cycle is. Paste this into the Chrome dev tools:

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

    Here's the test:

    > a = {}
    > b = {}
    > a.b = b; b.a = a;
    > isCyclic(a)
      Object {a: Object}
       "cycle at a"
      Object {b: Object}
       "cycle at b"
      true
    
    0 讨论(0)
  • 2020-11-29 00:21

    I converted the answer of Freddie Nfbnm to 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;
        }
    
    }
    
    0 讨论(0)
  • 2020-11-29 00:23

    You can also use Symbols - thanks to that approach you won't have to mutate properties of the original object, apart from adding symbol for marking visited node.

    It's cleaner and should be faster than gathering node properties and comparing with the object. It also has optional depth limitation if you don't want to serialize big nested values:

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

    This will result in an object that has all the circular dependencies stripped and also does not go deeper than given MAX_CLEANUP_DEPTH.

    Using symbols is safe as long as you don't do any meta-programming stuff on the object - they are transparent and they are not enumerable, hence - they will not show in any standard operations on the object.

    Also, returning a new, cleaned up object has an advantage of not mutating the original one if you need to perform any additional operations on it.

    If you don't want CIRCULAR marking, you can just modify the code a bit, hence skipping object before actually performing operations on it (inside the loop):

     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;
      }
     }
    
    0 讨论(0)
提交回复
热议问题