0

0

JavaScript中图结构JSON序列化:处理Map、Set与循环引用

花韻仙語

花韻仙語

发布时间:2025-10-28 15:31:13

|

700人浏览过

|

来源于php中文网

原创

javascript中图结构json序列化:处理map、set与循环引用

本文探讨了JavaScript中包含嵌套Set的Map对象(如图结构)在进行JSON序列化时遇到的挑战,包括Map和Set无法直接序列化以及循环引用导致的溢出错误。核心解决方案是通过在自定义类中实现toJSON()方法,将非标准数据结构转换为可序列化的形式,并巧妙地打破循环引用,从而实现图结构的正确、友好输出。

理解JavaScript对象JSON序列化的限制

在JavaScript中,JSON.stringify()是一个将JavaScript值转换为JSON字符串的常用方法。然而,它并非万能,尤其在处理复杂的数据结构时会遇到限制。具体来说,当尝试序列化一个包含Map、Set或存在循环引用的对象时,JSON.stringify()会表现出非预期行为或抛出错误。

考虑一个典型的图结构实现,其中Graph类使用Map来存储节点,而每个Node类又使用Set来存储其相邻节点。

class Node {
  constructor(value) {
    this.value = value;
    this.adjacents = new Set(); // 存储相邻节点对象的Set
  }

  addAdjacent(node) {
    this.adjacents.add(node);
  }
}

class Graph {
  constructor(directed = false) {
    this.nodes = new Map(); // 存储节点对象的Map
    this.directed = directed;
  }

  addVertex(value) {
    const node = this.nodes.has(value);
    if (node) {
      return this.nodes.get(value);
    }

    const vertex = new Node(value);
    this.nodes.set(value, vertex);
    return vertex;
  }

  addEdge(src, dest) {
    let srcNode = this.nodes.get(src);
    if (!srcNode) {
      srcNode = this.addVertex(src);
    }

    let destNode = this.nodes.get(dest);
    if (!destNode) {
      destNode = this.addVertex(dest);
    }

    srcNode.addAdjacent(destNode);
    if (this.directed === false) {
      destNode.addAdjacent(srcNode); // 无向图存在循环引用
    }
  }
}

const g1 = new Graph();
g1.addVertex("a");
g1.addVertex("b");
g1.addEdge("a", "c"); // 'a'与'c'相连,'c'与'a'相连(无向图)

console.log(g1);
/* 
输出示例:
Graph {
  nodes: Map(3) {
    'a' => Node { value: 'a', adjacents: [Set] },
    'b' => Node { value: 'b', adjacents: Set(0) {} },
    'c' => Node { value: 'c', adjacents: [Set] }
  },
  directed: false
}
*/

直接打印g1对象时,可以看到Map和Set类型的信息,但其内部数据(特别是Set中的具体元素)并未完全展开。当尝试使用JSON.stringify(g1)进行序列化时,会遇到两个主要问题:

立即学习Java免费学习笔记(深入)”;

  1. Map和Set无法直接序列化: JSON.stringify()默认只处理基本类型、普通对象和数组。Map和Set实例会被忽略或序列化为空对象(取决于具体环境和replacer函数的使用)。
  2. 循环引用: 在无向图中,如果节点A连接到节点B,那么节点B也连接到节点A。这意味着Node对象之间存在循环引用(例如,a.adjacents包含c,而c.adjacents包含a)。JSON.stringify()在遇到循环引用时,会尝试无限递归,最终导致RangeError: Maximum call stack size exceeded错误。

尝试通过replacer函数解决Set问题,但未能解决循环引用:

// 尝试将Map转换为普通对象,并处理Set
// console.log(
//   JSON.stringify(
//     Object.fromEntries(g1.nodes), // 将Map转换为普通对象
//     (_key, value) =>
//       value.adjacents instanceof Set ? [...value.adjacents] : value, // 将Set转换为数组
//     2
//   )
// );
// 这会导致 RangeError: Maximum call stack size exceeded

上述尝试失败的原因在于,即使将Map转换为对象,Set转换为数组,adjacents数组中仍然存储的是Node对象的引用,这些引用又包含对其他Node的引用,形成了循环,导致无限递归。

使用toJSON()方法定制序列化行为

JavaScript对象提供了一个特殊的toJSON()方法,当对象被JSON.stringify()序列化时,如果对象定义了这个方法,JSON.stringify()会调用它来获取一个可序列化的表示,而不是直接序列化原始对象。这是解决上述问题的关键。

我们可以为Node和Graph类分别实现toJSON()方法,以实现以下目标:

一帧秒创
一帧秒创

基于秒创AIGC引擎的AI内容生成平台,图文转视频,无需剪辑,一键成片,零门槛创作视频。

下载
  1. 将Map和Set转换为可序列化的普通对象或数组。
  2. 打破循环引用,通常通过将对象引用替换为其唯一标识符(如value属性)。

1. 为Node类实现toJSON()

在Node类中,adjacents是一个包含其他Node对象的Set。为了打破循环引用并使其可序列化,我们可以将其转换为一个包含相邻节点value(字符串)的数组。

class Node {
  constructor(value) {
    this.value = value;
    this.adjacents = new Set();
  }

  addAdjacent(node) {
    this.adjacents.add(node);
  }

  // 当Node对象被JSON.stringify序列化时调用
  toJSON() {
    return {
      value: this.value,
      // 将adjacents Set转换为一个包含相邻节点value的数组
      // 这打破了循环引用,因为不再直接引用Node对象
      adjacents: [...this.adjacents].map(({ value }) => value),
    };
  }
}

现在,当JSON.stringify()遇到一个Node对象时,它会调用toJSON(),返回一个包含value和adjacents(一个字符串数组)的普通对象。这样就避免了对完整Node对象的循环引用。

2. 为Graph类实现toJSON()

在Graph类中,nodes是一个Map,其中键是节点的值,值是Node对象。为了使其可序列化,我们可以将这个Map转换为一个普通JavaScript对象,其中键是节点的值,值是经过toJSON()处理后的Node对象。

class Graph {
  constructor(directed = false) {
    this.nodes = new Map();
    this.directed = directed;
  }

  addVertex(value) {
    const node = this.nodes.has(value);
    if (node) {
      return this.nodes.get(value);
    }

    const vertex = new Node(value);
    this.nodes.set(value, vertex);
    return vertex;
  }

  addEdge(src, dest) {
    let srcNode = this.nodes.get(src);
    if (!srcNode) {
      srcNode = this.addVertex(src);
    }

    let destNode = this.nodes.get(dest);
    if (!destNode) {
      destNode = this.addVertex(dest);
    }

    srcNode.addAdjacent(destNode);
    if (this.directed === false) {
      destNode.addAdjacent(srcNode);
    }
  }

  // 当Graph对象被JSON.stringify序列化时调用
  toJSON() {
    return {
      directed: this.directed,
      // 将nodes Map转换为一个普通对象
      // Object.fromEntries会将Map的键值对转换为对象的属性和值
      // 这里的value是Node对象,JSON.stringify会自动调用其toJSON方法
      nodes: Object.fromEntries(this.nodes),
    };
  }
}

通过Object.fromEntries(this.nodes),Map被转换为一个普通对象。由于这个普通对象的属性值是Node实例,JSON.stringify()会递归地调用这些Node实例的toJSON()方法,从而得到一个完全可序列化的结构。

完整示例与输出

将上述修改后的Node和Graph类结合,并进行序列化:

// 重新定义Node类
class Node {
  constructor(value) {
    this.value = value;
    this.adjacents = new Set();
  }

  addAdjacent(node) {
    this.adjacents.add(node);
  }

  toJSON() {
    return {
      value: this.value,
      adjacents: [...this.adjacents].map(({ value }) => value),
    };
  }
}

// 重新定义Graph类
class Graph {
  constructor(directed = false) {
    this.nodes = new Map();
    this.directed = directed;
  }

  addVertex(value) {
    const node = this.nodes.has(value);
    if (node) {
      return this.nodes.get(value);
    }

    const vertex = new Node(value);
    this.nodes.set(value, vertex);
    return vertex;
  }

  addEdge(src, dest) {
    let srcNode = this.nodes.get(src);
    if (!srcNode) {
      srcNode = this.addVertex(src);
    }

    let destNode = this.nodes.get(dest);
    if (!destNode) {
      destNode = this.addVertex(dest);
    }

    srcNode.addAdjacent(destNode);
    if (this.directed === false) {
      destNode.addAdjacent(srcNode);
    }
  }

  toJSON() {
    return {
      directed: this.directed,
      nodes: Object.fromEntries(this.nodes),
    };
  }
}

const g1 = new Graph();
g1.addVertex("a");
g1.addVertex("b");
g1.addEdge("a", "c");

console.log(JSON.stringify(g1, null, 2));

输出结果将是:

{
  "directed": false,
  "nodes": {
    "a": {
      "value": "a",
      "adjacents": [
        "c"
      ]
    },
    "b": {
      "value": "b",
      "adjacents": []
    },
    "c": {
      "value": "c",
      "adjacents": [
        "a"
      ]
    }
  }
}

这个JSON字符串清晰地展示了图的结构,包括每个节点的值及其相邻节点(以字符串形式表示),并且避免了任何序列化错误。

注意事项与总结

  • toJSON()的强大: toJSON()方法是JavaScript提供的一个强大机制,允许开发者完全控制自定义对象如何被JSON.stringify()序列化。
  • 处理循环引用: 解决循环引用最常见的方法是,在toJSON()方法中将对象引用替换为它们的唯一标识符(如ID、名称或值)。这样,JSON中存储的是标识符而不是完整的对象,从而打破了循环。
  • 转换非标准类型: Map和Set等非标准数据结构需要手动转换为普通对象或数组才能被JSON.stringify()正确处理。Object.fromEntries()和展开运算符[...set]是常用的转换工具
  • 数据丢失风险: 在打破循环引用时,需要权衡。将对象引用替换为标识符意味着在JSON中丢失了原始对象的完整信息(例如,你无法从"c"这个字符串直接获取c节点的完整属性)。如果需要反序列化回完整的图结构,可能需要额外的逻辑来根据这些标识符重新构建对象关系。

通过在自定义类中巧妙地实现toJSON()方法,我们可以有效地管理复杂数据结构(如图)的JSON序列化过程,解决Map、Set等非标准类型以及循环引用带来的挑战,生成结构清晰、易于理解和传输的JSON数据。

热门AI工具

更多
DeepSeek
DeepSeek

幻方量化公司旗下的开源大模型平台

豆包大模型
豆包大模型

字节跳动自主研发的一系列大型语言模型

通义千问
通义千问

阿里巴巴推出的全能AI助手

腾讯元宝
腾讯元宝

腾讯混元平台推出的AI助手

文心一言
文心一言

文心一言是百度开发的AI聊天机器人,通过对话可以生成各种形式的内容。

讯飞写作
讯飞写作

基于讯飞星火大模型的AI写作工具,可以快速生成新闻稿件、品宣文案、工作总结、心得体会等各种文文稿

即梦AI
即梦AI

一站式AI创作平台,免费AI图片和视频生成。

ChatGPT
ChatGPT

最最强大的AI聊天机器人程序,ChatGPT不单是聊天机器人,还能进行撰写邮件、视频脚本、文案、翻译、代码等任务。

相关专题

更多
json数据格式
json数据格式

JSON是一种轻量级的数据交换格式。本专题为大家带来json数据格式相关文章,帮助大家解决问题。

455

2023.08.07

json是什么
json是什么

JSON是一种轻量级的数据交换格式,具有简洁、易读、跨平台和语言的特点,JSON数据是通过键值对的方式进行组织,其中键是字符串,值可以是字符串、数值、布尔值、数组、对象或者null,在Web开发、数据交换和配置文件等方面得到广泛应用。本专题为大家提供json相关的文章、下载、课程内容,供大家免费下载体验。

546

2023.08.23

jquery怎么操作json
jquery怎么操作json

操作的方法有:1、“$.parseJSON(jsonString)”2、“$.getJSON(url, data, success)”;3、“$.each(obj, callback)”;4、“$.ajax()”。更多jquery怎么操作json的详细内容,可以访问本专题下面的文章。

334

2023.10.13

go语言处理json数据方法
go语言处理json数据方法

本专题整合了go语言中处理json数据方法,阅读专题下面的文章了解更多详细内容。

82

2025.09.10

java基础知识汇总
java基础知识汇总

java基础知识有Java的历史和特点、Java的开发环境、Java的基本数据类型、变量和常量、运算符和表达式、控制语句、数组和字符串等等知识点。想要知道更多关于java基础知识的朋友,请阅读本专题下面的的有关文章,欢迎大家来php中文网学习。

1566

2023.10.24

Go语言中的运算符有哪些
Go语言中的运算符有哪些

Go语言中的运算符有:1、加法运算符;2、减法运算符;3、乘法运算符;4、除法运算符;5、取余运算符;6、比较运算符;7、位运算符;8、按位与运算符;9、按位或运算符;10、按位异或运算符等等。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

241

2024.02.23

php三元运算符用法
php三元运算符用法

本专题整合了php三元运算符相关教程,阅读专题下面的文章了解更多详细内容。

148

2025.10.17

mysql标识符无效错误怎么解决
mysql标识符无效错误怎么解决

mysql标识符无效错误的解决办法:1、检查标识符是否被其他表或数据库使用;2、检查标识符是否包含特殊字符;3、使用引号包裹标识符;4、使用反引号包裹标识符;5、检查MySQL的配置文件等等。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

210

2023.12.04

C# ASP.NET Core微服务架构与API网关实践
C# ASP.NET Core微服务架构与API网关实践

本专题围绕 C# 在现代后端架构中的微服务实践展开,系统讲解基于 ASP.NET Core 构建可扩展服务体系的核心方法。内容涵盖服务拆分策略、RESTful API 设计、服务间通信、API 网关统一入口管理以及服务治理机制。通过真实项目案例,帮助开发者掌握构建高可用微服务系统的关键技术,提高系统的可扩展性与维护效率。

3

2026.03.11

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
React 教程
React 教程

共58课时 | 6万人学习

TypeScript 教程
TypeScript 教程

共19课时 | 3.4万人学习

Bootstrap 5教程
Bootstrap 5教程

共46课时 | 3.6万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号