在 JavaScript 中检测和修复循环引用

假设我在一个大型 JavaScript 对象中有一个循环引用

我试试 JSON.stringify(problematicObject)

然后浏览器就会抛出

“ TypeError: 将循环结构转换为 JSON”

(预计)

然后我想找到这个循环参考的原因,最好是使用 Chrome 开发工具?这可能吗?如何在大型对象中查找和修复循环引用?

62803 次浏览

尝试在 chrome/firefox 浏览器上使用 console.log()来识别问题出现的位置。

在使用 Firebug 插件的 Firefox 上,您可以逐行调试您的 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));

参考例子 网站: http://jsfiddle.net/SivaCharan/AsPK7/”rel = “ nofollow”> LIVE DEMO

这是我刚做的,可能有点脏,但还是能用..

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

这表示 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,[]));
}

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 中复制了一个对象,它将返回 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 && 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

循环参考检测器

下面是我的 循环参考检测器类,它输出循环引用值实际位于的所有属性堆栈信息,并显示元凶引用的位置。

这是特别有用的巨大的结构,它是不明显的关键价值是危害的来源。

它输出循环引用的值字符串化,但是所有对自身的引用都替换为“[循环对象——-fix me ]”。

用法:
CircularReferenceDetector.detectCircularReferences(value);

注: 如果不想使用任何日志记录器或者没有可用的日志记录器,请删除 Logger.* 语句。

技术说明:
递归函数遍历对象的所有属性,并测试 JSON.stringify 是否成功。 如果它没有成功(循环引用) ,那么它通过用某个常量字符串替换 value 本身来测试它是否成功。这意味着如果它成功地使用了这个替换器,那么这个值就是被循环引用的值。如果不是,它将递归地遍历该对象的所有属性。

同时,它还跟踪属性堆栈,为您提供罪魁祸首值所在位置的信息。

打印稿

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;

This is a fix for both @ Trey Mack and @ Freddie Nfbnm answers on the typeof obj != 'object' condition. Instead it should test if the obj value is not instance of object, so that it can also work when checking values with object familiarity (for example, functions 和符号 (symbols aren't instance of object, but still addressed, btw.)).

因为我还不能在这个 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;
}

以下是适用于节点的 @ 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;
}


}

也可以将 JSON.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
})

这里有很多答案,但我觉得我应该加入我的解决方案。它类似于 @ 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

下面是 @ dkurzaj 的密码的混音版本(本身就是@Aaron V,@user4976005,@Trey Mack 和最后@Freddie Nfbnm 的混音版本)代码)加上 @ dark singe 的 WeakMap创意。所以... 这个线程是 Megamix,我猜:)

在我的版本中,报表(而不是 console.log的条目)可选地作为对象数组返回。如果不需要报告,则在第一次看到循环引用(a‘ la@dark singe’s code)时停止测试。

此外,由于 Object.keys只返回 hasOwnProperty属性(参见: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/keys) ,因此 hasOwnProperty已被删除。

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

下面是 MDN 在循环对象上使用 JSON.stringify()时检测和修复循环引用的方法: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/Cyclic_object_value:

像下面这样的圆形结构

var circularReference = {otherData: 123};
circularReference.myself = circularReference;

JSON.stringify()将会倒闭:

JSON.stringify(circularReference);
// TypeError: cyclic object value

要序列化循环引用,可以使用支持循环引用的库(例如 Cycle.js)或自己实现解决方案,这需要通过可序列化的值查找和替换(或删除)循环引用。

下面的代码片段说明了如何使用 JSON.stringify()的替代参数查找和过滤循环引用(因此导致数据丢失) :

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

这将导致剥离所有循环依赖关系的对象,并且不会比给定的 MAX_CLEANUP_DEPTH更深。

使用符号是安全的,只要你不对对象进行任何元编程——它们是透明的,不可枚举,因此——它们不会在对象的任何标准操作中显示。

另外,返回一个新的、清理干净的对象的优点是,如果需要对原始对象执行任何其他操作,则不会对其进行变异。

如果你不想要 CIRCULAR标记,你可以稍微修改一下代码,因此在实际执行操作之前跳过对象(在循环中) :

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

大多数其他的答案只说明如何 侦查一个对象树有一个循环引用-他们没有告诉你如何 修好这些循环引用(即。将循环引用值替换为,例如。undefined).

下面是用 undefined替换所有循环引用的函数:

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

对于专门用于 JSON.stringify的函数,只需在字符串化之前调用上面的函数(注意,它会使传入的对象发生变化) :

const objTree = {normalProp: true};
objTree.selfReference = objTree;
RemoveCircularLinks(objTree); // without this line, the JSON.stringify call errors
console.log(JSON.stringify(objTree));

如果您只是需要查看该循环对象的内容,那么只需使用 console.table (cirarObj)