program story

JavaScript에서 순환 참조 감지 및 수정

inputbox 2021. 1. 7. 07:57
반응형

JavaScript에서 순환 참조 감지 및 수정


큰 JavaScript 객체에 순환 참조가있는 경우

그리고 나는 시도 JSON.stringify(problematicObject)

그리고 브라우저는

"TypeError : 원형 구조를 JSON으로 변환"

(예상)

그렇다면 Chrome 개발자 도구를 사용하여이 순환 참조의 원인을 찾고 싶습니다. 이것이 가능한가? 큰 개체에서 순환 참조를 어떻게 찾고 수정합니까?


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

@tmack의 대답은 확실히 내가이 질문을 발견했을 때 찾던 것입니다!

불행히도 많은 거짓 긍정을 반환합니다. 객체가 JSON에서 복제되면 참을 반환하며 이는 순환 성과 동일하지 않습니다 . 순환 성은 객체가 자신의 자식임을 의미합니다.

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

CircularReferenceDetector

다음은 순환 참조 된 값이 실제로있는 모든 속성 스택 정보를 출력하고 범인 참조가있는 위치를 보여주는 CircularReferenceDetector 클래스입니다.

이것은 어떤 값이 피해의 원인인지 키로 명확하지 않은 거대한 구조물에 특히 유용합니다.

순환 참조 된 값을 문자열로 출력하지만 자체에 대한 모든 참조는 "[Circular object --- fix me]"로 대체됩니다.

용법:
CircularReferenceDetector.detectCircularReferences(value);

참고 : 로깅을 사용하지 않거나 사용 가능한 로거가없는 경우 Logger. * 문을 제거하십시오.

기술적 설명 :
재귀 함수는 객체의 모든 속성을 살펴보고 JSON.stringify가 성공하는지 여부를 테스트합니다. 성공하지 못하면 (순환 참조) 값 자체를 상수 문자열로 대체하여 성공 여부를 테스트합니다. 이는이 대체자를 사용하여 성공하면이 값이 순환 참조되는 값임을 의미합니다. 그렇지 않은 경우 해당 개체의 모든 속성을 반복적으로 통과합니다.

한편 속성 스택을 추적하여 범인 값이있는 위치에 대한 정보를 제공합니다.

타이프 스크립트

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;


이것은 조건에 대한 @Trey Mack@Freddie Nfbnm 답변 모두에 대한 수정 사항입니다 typeof obj != 'object'. 대신 obj값이 객체의 인스턴스가 아닌지 테스트 하여 객체에 익숙한 값을 확인할 때도 작동 할 수 있습니다 (예 : 함수 및 기호 (기호는 객체의 인스턴스가 아니지만 여전히 주소가 지정됨)).

이 StackExchange 계정에 아직 댓글을 달 수 없기 때문에 답변으로 게시하고 있습니다.

추신 :이 답변을 삭제하도록 요청하십시오.

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

try / catchJSON.stringify 와 함께 사용할 수도 있습니다.

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


다음은 @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
})

방금 만들었어요. 더러울 수 있지만 어쨌든 작동합니다 ... : 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

이것은 cx [3]이 c와 같고 cx = cy [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,[]));
}

다음은 노드에 적합한 @Thomas의 답변 입니다.

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

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

}

객체를 기록하려는 경우 Circular-JSON https://github.com/WebReflection/circular-json 이 적합 할 수 있습니다.


여기에 많은 답변이 있지만 믹스에 내 솔루션을 추가 할 것이라고 생각했습니다. @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

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

ReferenceURL : https://stackoverflow.com/questions/14962018/detecting-and-fixing-circular-references-in-javascript

반응형