检测和修复JavaScript中的循环引用
假设我在一个大型JavaScript对象中有一个循环引用 我尝试检测和修复JavaScript中的循环引用,javascript,Javascript,假设我在一个大型JavaScript对象中有一个循环引用 我尝试JSON.stringify(problematicObject) 浏览器会抛出 “TypeError:将循环结构转换为JSON” (这是预期的) 然后我想找到这个循环引用的原因,最好使用Chrome开发者工具?这可能吗?如何在大型对象中查找和修复循环引用 尝试在chrome/firefox浏览器上使用console.log(),以确定遇到问题的位置 在Firefox上使用Firebug插件,您可以逐行调试javascript 更新
JSON.stringify(problematicObject)
浏览器会抛出
“TypeError:将循环结构转换为JSON”
(这是预期的)
然后我想找到这个循环引用的原因,最好使用Chrome开发者工具?这可能吗?如何在大型对象中查找和修复循环引用 尝试在chrome/firefox浏览器上使用
console.log()
,以确定遇到问题的位置
在Firefox上使用Firebug插件,您可以逐行调试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));
参考示例我刚刚做了这个。它可能很脏,但无论如何都能用…: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,'>'));
}
然后
说
这说明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,[]));
}
从…拉出来。添加一行以检测循环的位置。将其粘贴到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中复制了一个对象,则返回true,这与循环性不同。圆形意味着一个对象是它自己的子对象,例如
obj.key1.key2.[...].keyX === obj
我修改了原始答案,这对我来说很有用:
功能是循环的(obj){
var键=[];
var堆栈=[];
var stackSet=new Set();
检测到的var=假;
功能检测(obj,钥匙){
if(obj&&typeof obj!=“object”){return;}
如果(stackSet.has(obj)){//它是循环的!打印对象及其位置。
var oldindex=stack.indexOf(obj);
var l1=keys.join('.')+'.+键;
var l2=keys.slice(0,oldindex+1).join('.');
log('循环:'+l1+'='+l2+'='+obj);
控制台日志(obj);
检测到=真;
返回;
}
按键。按(键);
堆栈推送(obj);
stackSet.add(obj);
对于(obj中的var k){//在对象的子对象上俯冲
if(Object.prototype.hasOwnProperty.call(obj,k)){detect(obj[k],k);}
}
keys.pop();
stack.pop();
stackSet.delete(obj);
返回;
}
探测(obj,'obj');
检测到返回;
}
以下是一些非常简单的测试:
var root={}
var leaf={'isleaf':true};
var cycle2={l:leaf};
var cycle1={c2:cycle2,l:leaf};
循环2.c1=循环1
根叶=叶
i循环(循环1);//返回true,记录“循环:obj.c2.c1=obj”
Iscycle(cycle2);//返回true,记录“循环:obj.c1.c2=obj”
Iscycle(叶);//返回false
Iscycle(根);//返回false
循环引用检测器
下面是我的CircularReferenceDetector类,它输出循环引用值实际所在的所有属性堆栈信息,并显示罪魁祸首引用的位置
这对于大型结构物尤其有用,因为在这些结构物中,键不明显哪个值是危害的来源
它输出字符串化的循环引用值,但对自身的所有引用都替换为“[Circular object---fix me]”
用法:
循环引用检测器。检测循环引用(值)代码>
注意:
如果不想使用任何日志记录或没有可用的日志记录程序,请删除Logger.*语句
技术说明:
递归函数将检查对象的所有属性,并测试JSON.stringify是否在这些属性上成功。
如果它没有成功(循环引用),那么它将通过用某个常量字符串替换值本身来测试它是否成功。这意味着,如果它成功地使用了这个替换项,那么这个值就是循环引用的值。如果不是,它将递归地遍历该对象的所有属性
同时,它还跟踪属性堆栈,为您提供罪魁祸首值所在位置的信息
打字稿
从“./Logger”导入{Logger};
导出类循环引用检测器{
静态检测循环引用(tobestringiedvalue:any,serializationKeyStack:string[]=[])){
key(tobestringiedValue).forEach(key=>{
var值=TobestringiedValue[关键];
var serializationKeyStackWithNewKey=serializationKeyStack.slice();
serializationKeyStackWithNewKey.push(按键);
试一试{
stringify(value);
debug(`path“${Util.joinStrings(serializationKeyStack)}”是ok`);
}捕获(错误){
Logger.debug(`path“${Util.joinStrings(serializationKeyStack)}”JSON.stringify导致错误:${error}`);
var isCircularValue:布尔值;
var circularExcludingStringifyResult:string=“”;
试一试{
circularExcludingStringifyResult=JSON.stringify(值,CircularReferenceDetector.replaceRootStringifyReplacer(值),2);
isCircularValue=true;
}捕获(错误){
Logger.debug(`path“${Util.joinStrings(serializationKeyStack)}”不是循环源`);
循环引用检测器。检测循环引用(值,serializationKeyStackWithNewKey);
isCircularValue=false;
}
if(isCircularValue){
抛出新错误(`Circular reference detected:\n循环引用的值是给定根对象的路径“${Util.joinStrings(serializationKeyStackWithNewKey)}”下的值\n`+
`对该值调用stringify,但将其自身替换为[Circular object---fix me](`${v1}${separator}${v2}`);
}
}
从TypeScript编译的JavaScript
“严格使用”;
> a = {}
> b = {}
> a.b = b; b.a = a;
> isCyclic(a)
Object {a: Object}
"cycle at a"
Object {b: Object}
"cycle at b"
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 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;
}
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;
}
}
}
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;
}
}
function hasCircularDependency(obj)
{
try
{
JSON.stringify(obj);
}
catch(e)
{
return e.includes("Converting circular structure to JSON");
}
return false;
}
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
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);
}
var circularReference = {otherData: 123};
circularReference.myself = circularReference;
JSON.stringify(circularReference);
// TypeError: cyclic object value
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}
// 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;
}
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;
}
}
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);
}
const objTree = {normalProp: true};
objTree.selfReference = objTree;
RemoveCircularLinks(objTree); // without this line, the JSON.stringify call errors
console.log(JSON.stringify(objTree));