首页 > web前端 > js教程 > 正文

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

DDD
发布: 2025-10-28 12:30:01
原创
631人浏览过

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

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

理解JSON序列化的挑战

在JavaScript中,将复杂对象转换为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错误,即堆栈溢出。

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

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

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() 方法

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

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

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

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

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

1. 改造 Node 类

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

Find JSON Path Online
Find JSON Path Online

Easily find JSON paths within JSON objects using our intuitive Json Path Finder

Find JSON Path Online30
查看详情 Find JSON Path Online
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转换为一个普通的JavaScript对象,其中键是节点值,值是经过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字符串重建原始JavaScript对象),通常需要实现一个对应的反序列化函数,该函数将根据JSON结构手动重建Map、Set以及建立正确的循环引用。

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

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

最佳 Windows 性能的顶级免费优化软件
最佳 Windows 性能的顶级免费优化软件

每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。

下载
来源:php中文网
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn
最新问题
开源免费商场系统广告
热门教程
更多>
最新下载
更多>
网站特效
网站源码
网站素材
前端模板
关于我们 免责申明 意见反馈 讲师合作 广告合作 最新更新 English
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送
PHP中文网APP
随时随地碎片化学习
PHP中文网抖音号
发现有趣的

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