Detecting and fixing circular references in JavaScript

后端 未结 15 812
庸人自扰
庸人自扰 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: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;
      }
     }
    

提交回复
热议问题