新闻中心

J*aScript中Map与Set及循环引用对象的JSON序列化教程

2025-10-28
浏览次数:
返回列表

JavaScript中Map与Set及循环引用对象的JSON序列化教程

本教程旨在解决j*ascript中包含`map`、`set`以及循环引用等复杂数据结构的对象的json序列化问题。我们将探讨`json.stringify()`直接处理这些结构时遇到的挑战,特别是循环引用导致的堆栈溢出错误。核心解决方案是利用j*ascript对象的`tojson()`方法,通过自定义序列化逻辑,将非标准或循环引用的数据结构转换为可被`json.stringify()`正确处理的普通j*ascript对象或基本类型,从而实现复杂对象的完整且友好的json输出。

理解JSON序列化的挑战

在J*aScript中,将复杂对象转换为JSON字符串是常见的操作,通常使用JSON.stringify()方法。然而,当对象中包含Map、Set等ES6引入的数据结构,或者存在循环引用时,JSON.stringify()的默认行为会遇到障碍。

1. Map和Set的默认序列化问题

JSON.stringify()在处理Map和Set实例时,并不会将其内部元素直接序列化为JSON数组或对象。例如,Map会被序列化为空对象{},而Set则会被序列化为空对象{}或一个带有[Set]标识的非详细表示(取决于环境的console.log行为,而非实际JSON序列化)。这意味着,如果直接尝试序列化包含Map或Set的对象,这些数据结构内部的实际数据将丢失。

2. 循环引用导致的堆栈溢出

更严重的问题是循环引用。在图结构(如示例中的Graph类)中,节点之间通常存在相互引用,例如节点A指向节点C,同时节点C也指向节点A。当JSON.stringify()尝试序列化一个包含循环引用的对象时,它会陷入无限递归,因为它试图遍历并序列化一个永无止境的引用链。这会导致RangeError: Maximum call stack size exceeded错误,即堆栈溢出。

考虑以下图结构的代码示例:

class Node {
  constructor(value) {
    this.value = value;
    this.adjacents = new Set(); // 存储相邻节点,可能包含循环引用
  }

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

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

  addVertex(value) {
    if (this.nodes.has(value)) {
      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(JSON.stringify(g1, null, 2));

直接对g1调用JSON.stringify(),会因为Node对象中的adjacents Set包含Node对象本身,以及Graph中的nodes Map包含Node对象,且节点间存在循环引用(例如a与c互为邻接),从而引发堆栈溢出错误。

解决方案:利用 toJSON() 方法

J*aScript提供了一个强大的机制来解决自定义对象的JSON序列化问题:toJSON()方法。如果一个对象拥有toJSON()方法,JSON.stringify()在序列化该对象时,会优先调用这个方法,并使用其返回值进行序列化,而不是直接序列化原始对象。这为我们提供了自定义序列化逻辑的入口。

通过在类中实现toJSON()方法,我们可以:

  • 将Map和Set等非标准数据结构转换为普通数组或对象。
  • 将循环引用的对象替换为它们的标识符(如value属性),从而打破循环。

实现自定义序列化:修改 Node 和 Graph 类

为了正确序列化Graph对象,我们需要在Node和Graph类中分别实现toJSON()方法。

1. 改造 Node 类

Node类中的adjacents是一个Set,其中包含其他Node对象的引用。为了打破循环引用并正确序列化,我们需要将Set转换为一个包含相邻节点值的普通数组。

万相营造 万相营造

阿里妈妈推出的AI电商营销工具

万相营造 168 查看详情 万相营造
class Node {
  constructor(value) {
    this.value = value;
    this.adjacents = new Set();
  }

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

  // 实现 toJSON 方法
  toJSON() {
    return {
      value: this.value,
      // 将 adjacents Set 转换为一个包含相邻节点值的数组
      // 这打破了循环引用,因为我们只存储值而不是完整的Node对象
      adjacents: [...this.adjacents].map(({ value }) => value),
    };
  }
}

在Node的toJSON()方法中:

  • 我们返回一个新的对象,包含value属性。
  • adjacents属性被设置为一个新数组。[...this.adjacents]将Set转换为一个包含Node对象的数组。
  • .map(({ value }) => value)进一步遍历这个数组,只提取每个相邻Node的value属性。这样做既解决了Set的序列化问题,又通过只保留节点标识符(而非完整节点对象)打破了潜在的循环引用。

2. 改造 Graph 类

Graph类中的nodes是一个Map,其键是节点值,值是Node对象。我们需要将这个Map转换为一个普通的J*aScript对象,其中键是节点值,值是经过Node.toJSON()处理后的节点表示。

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

  addVertex(value) {
    if (this.nodes.has(value)) {
      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 方法
  toJSON() {
    return {
      directed: this.directed,
      // 将 nodes Map 转换为一个普通对象
      // Object.fromEntries() 将 Map 的 [key, value] 对转换为 { key: value }
      // 由于 Map 的值是 Node 对象,它们会递归调用自身的 toJSON 方法
      nodes: Object.fromEntries(this.nodes),
    };
  }
}

在Graph的toJSON()方法中:

  • 我们返回一个新对象,包含directed属性。
  • nodes属性被设置为一个新对象,通过Object.fromEntries(this.nodes)将Map转换为一个普通对象。
  • 关键在于,Object.fromEntries()在处理Map的值(即Node对象)时,JSON.stringify()会递归地查找这些Node对象上的toJSON()方法并调用它,从而确保每个节点都被正确序列化为我们期望的格式。

完整示例与输出

现在,结合修改后的Node和Graph类,我们可以安全地序列化Graph对象并获得清晰的JSON输出。

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

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

  addVertex(value) {
    if (this.nodes.has(value)) {
      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"); // a -> c, c -> a (因为是无向图)

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

输出结果:

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

现在,Graph对象被成功序列化为一个可读性强、结构清晰的JSON字符串。每个节点的adjacents列表都显示了其相邻节点的值,Map结构也被正确转换为对象,并且没有出现堆栈溢出错误。

注意事项与总结

  • toJSON()的优先级:JSON.stringify()在序列化对象时会优先查找并调用对象的toJSON()方法。这是自定义序列化行为的关键。
  • 打破循环引用:在设计toJSON()方法时,对于可能导致循环引用的属性(如图中的adjacents),务必将其转换为基本类型值或标识符,而不是包含完整对象的引用。
  • 数据结构转换:Map和Set需要显式地转换为可序列化的数据结构,如普通对象或数组。Object.fromEntries()和展开运算符...是常用的工具。
  • 递归调用:JSON.stringify()在处理一个包含自定义toJSON()方法的对象时,如果该对象内部又包含其他自定义toJSON()方法的对象,它会递归地调用这些内部对象的toJSON()方法,从而实现层层序列化。
  • 可逆性:请注意,通过toJSON()方法进行的序列化不一定是可逆的。为了反序列化(即从JSON字符串重建原始J*aScript对象),通常需要实现一个对应的反序列化函数,该函数将根据JSON结构手动重建Map、Set以及建立正确的循环引用。

通过有效地利用toJSON()方法,开发者可以完全控制复杂J*aScript对象的JSON序列化过程,确保输出的JSON既完整又符合预期,同时避免常见的序列化陷阱。

以上就是J*aScript中Map与Set及循环引用对象的JSON序列化教程的详细内容,更多请关注其它相关文章!


# 自定义  # 餐饮优惠营销推广文案  # 开熟食店营销推广方案  # 楼房建设信息查询网站  # 荆州网站建设路火锅  # 标题频繁改动网站优化  # 孝感网站建设网站推广  # 西华推广营销  # 营销推广网站图片制作  # 济宁哪个网站建设好点  # 淘宝如何计算关键词排名  # 而不是  # 遍历  # 一个普通  # 是一个  # 类中  # javascript  # 数据结构  # 转换为  # 递归  # 序列化  # json数组  # 堆栈溢出  #   # 工具  # edge  # node  # json  # js  # java  # es6 


相关栏目: 【 科技资讯46185 】 【 网络学院92790


相关推荐: word邮件合并后日期格式不对怎么改_Word邮件合并日期格式修改方法  Go语言中JSON数据解码与字段访问指南  文本文档写html代码怎么运行_文本文档html代码运行步骤【教程】  企业名称高精度匹配:N-gram方法在结构相似性分析中的应用  PyTorch模型训练准确率不提升:诊断与修复常见指标计算错误  照顾宝贝2小游戏点击立即在线玩  LINQ to XML为何解析失败? 深入理解C# XDocument的异常处理  Python模块化编程:有效管理依赖与避免循环引用  C++如何操作注册表_Windows平台下C++读写注册表的API函数详解  小米汽车11月交付量突破40000台!雷军:将继续努力  win11怎么查看应用耗电情况 Win11电池设置查看应用能耗排行榜【优化】  Bing引擎入口最新2025 Bing搜索免费官方登录  快手网页版在线登录 快手网页版官网入口快速访问  利用5118提升短视频内容效果_5118短视频关键词优化方法  CSS响应式网页如何实现主次模块比例自适应_flex-grow与flex-shrink调整  J*aScript数据结构转换:将对象数组按类别分组  QQ邮箱网页版快速登录 QQ邮箱邮箱账号官方入口地址  css子元素高度不一致导致布局错位怎么办_使用align-items:stretch解决高度差异  俄罗斯浏览器官网直达链接 俄罗斯浏览器最新在线入口导航  漫蛙2漫画入口 漫蛙正版网页漫画直达网址  韩剧圈正版入口页面_韩剧圈官网登录链接  为什么简单的XML文件也会解析失败? 检查隐藏的非打印字符(如BOM)的方法  qq浏览器如何查看和导出已保存的密码 qq浏览器密码管理器数据备份教程  学习通在线学习平台 学习通网页版直接进入课程中心  Composer中的^和~符号代表什么_精通Composer版本号语义化约束  Golang如何使用const iota_Go iota常量计数器讲解  整合Supabase认证与Django模型:跨模式迁移的解决方案  如何解决电商平台定制报价请求的“黑洞”问题,SprykerQuoteRequest模块助你提升客户体验与销售效率  C++如何操作大型数据集_使用C++流式处理(Streaming)技术避免一次性加载大文件  QQ邮箱电脑版登录入口_QQ邮箱官方网站登录平台  Discord Slash 命令响应超时问题的异步解决方案  163邮箱官方主页登录 直达网易邮箱登录核心页面  Win11怎么设置鼠标主按键_Win11鼠标左右键功能互换  深入理解J*aScript中的B样条曲线与节点向量生成  优化 Jest 模拟:强制未实现函数抛出错误以提升测试效率  优化MinIO list_objects_v2 操作的性能瓶颈与最佳实践  J*aScript 字符串标签转换:使用正则表达式高效替换  知音漫客正版漫画平台_知音漫客官网账号登录  如何在复杂的电商平台中优雅地管理共享资源并确保正确重定向,使用spryker-shop/resource-share-page模块助你一臂之力  AO3网页版最新入口合集 Archive of Our Own在线访问指南  c++如何使用std::memory_order控制原子操作顺序_c++ C++11内存模型详解  Lar*el如何正确地在控制器和模型之间分配逻辑_Lar*el代码职责分离与架构建议  Golang如何使用new_Go new分配内存机制讲解  Golang如何通过reflect操作map_Golang reflect map操作与遍历技巧  如何使 Jest 模拟函数默认抛出错误以提高测试效率  Golang如何测试channel通信行为_Golang channel通信测试与分析方法  CSS如何设置hover状态颜色_hover伪类调整背景或文字颜色  写好的html代码怎么运行出来_运行写好的html代码方法【教程】  Pyrogram与g4f集成:异步编程实践与常见错误解决  动漫花园资源网使用步骤_动漫花园资源网下载流程 

搜索