导读:本期聚焦于新井创作的《JavaScript 的 JSON.stringify 和 JSON.parse 方法在序列化复杂对象时有何限制?》,敬请观看详情。为什么 JSON.stringify 在某些场景下会静默丢失数据,而另一些场景下直接抛错?序列化复杂对象时,函数、undefined 和 Symbol 类型的属性会被忽略,NaN 和 Infinity 会被改写成 null。更棘手的是循环引用,只要对象形成环,序列化就会立刻失败。即使能完成序列化,parse 回来后的对象也不再拥有原来的原型链,Date、Map、Set 等内置类型的实例会退化成普通字符串或空对象,大整数还会面临精度丢失的风险。这篇文章从这些具体现象出发,逐一剖析 JSON 序列化机制的边界行为,并给出自定义 toJSON 和递归序列化等可行方案,帮助你在实际项目中正确选择数据交换格式,避免那些不容易排查的数据失真问题。

JSON 是目前前后端通信中使用最广泛的数据交换格式,它语法简单、可读性强,几乎所有编程语言都提供了原生的序列化与反序列化能力。在 JavaScript 中,标准做法是使用 JSON.stringify 将对象转成字符串,再用 JSON.parse 把字符串还原为对象。不过,这对组合在应对复杂对象时并不像看起来那么可靠:函数、循环引用、Map、Date 等特殊结构会被静默忽略、强转或者直接抛出异常。很多数据失真问题之所以难以排查,正是因为 JSON.stringify 在不同场景下表现出截然不同的处理规则。

JavaScript 的 JSON.stringify 和 JSON.parse 方法在序列化复杂对象时有何限制?

一、特殊类型值在序列化时的修正

JSON 格式自身只定义了对象、数组、字符串、数字、布尔值和 null 这几种数据类型,任何超出这个范围的值都需要被 JSON.stringify 做特殊处理。最典型的例子是函数、undefined 和 Symbol,它们作为对象属性时会被直接忽略,而作为数组元素时会被改写成 null。这种不一致很容易造成数据错位,尤其当业务代码依赖数组索引时,原本占位的 undefined 被替换成 null,还原后的数组结构和原始数组就可能产生语义差异。

const obj = {
  name: '张三',
  sayHello() {
    return 'hello';
  },
  undef: undefined,
  sym: Symbol('id'),
  nan: NaN,
  infinity: Infinity,
  date: new Date('2024-06-01T08:00:00.000Z'),
  reg: /abc/g,
};

const json = JSON.stringify(obj);
console.log(json);
// 输出:{"name":"张三","nan":null,"infinity":null,"date":"2024-06-01T08:00:00.000Z","reg":{}}

从输出结果可以清楚看到,sayHello、undef 和 sym 三个键直接消失了,而 NaN 和 Infinity 被强制转换成 null,Date 被转换成了 ISO 格式的字符串,正则表达式则被序列化成了一个空对象。如果依赖默认行为去保存配置信息或状态快照,这些数据还原后往往就无法工作。例如原本存储的正则表达式在 parse 之后会变成普通对象,调用 reg.test 就会直接报错。

这里还有一个容易被忽略的细节:当 undefined 出现在数组中时,JSON.stringify 会将其渲染成 null,而不是跳过占位。这意味着从数组角度来说,JSON 序列化实际上会“修正”数组的稀疏性,让稀疏数组的缺失项变成显式的 null 项。这种转换在多数场景下不会引发问题,但如果后端接口对空值和非空值的语义有严格区分,还原后的数据就可能会对业务逻辑产生误导。

二、循环引用导致序列化失败

复杂对象中经常存在互相引用的结构,比如一棵树中父节点持有子节点引用,子节点又持有 parent 指针回到父节点。对于这种对象,JSON.stringify 直接给出 TypeError,提示“转换循环结构到 JSON 时出错”。原因是 JSON 序列化的算法需要递归遍历所有可枚举属性,一旦遇到环,遍历就无法终止,引擎只能通过抛错来保护调用栈。

const parent = { name: '父节点', children: [] };
const child = { name: '子节点', parent };
parent.children.push(child);

try {
  JSON.stringify(parent);
} catch (error) {
  console.log(error.message);
  // 输出:Converting circular structure to JSON
}

这种限制对树形结构、链表、图结构的数据持久化影响很大。一个常见的解决思路是在序列化之前手动解除引用,例如在提交到后端前将 parent 字段替换成 id,解析时再根据 id 重新关联。但如果数据层级很深,手工处理非常容易遗漏,而且一旦后续代码中新增了循环引用,序列化又会立刻崩溃。

另一种更通用的策略是使用自定义的递归序列化函数,通过 WeakMap 记录已经访问过的对象,遇到重复引用时使用路径字符串标记或者直接忽略。不过这种做法必须结合业务需求去选择,因为在还原阶段,无法凭空恢复出完整的循环结构,除非在序列化时同时保存对象之间的引用映射。无论采用哪种方式,循环引用的存在都意味着直接使用 JSON.stringify 是不可行的。

三、原型链丢失与内置类型退化

JSON.parse 只能还原出纯对象和数组,它没有能力恢复自定义类的实例结构。一个 User 对象经过 JSON.stringify 再 JSON.parse 之后,得到的只是一个普通 Object,它的 constructor 已经变成 Object,原型上的方法自然全部丢失。这在传输层可能没有问题,但如果在客户端使用类实例接收响应,就不得不手动调用构造函数重新包装。

class User {
  constructor(id, name) {
    this.id = id;
    this.name = name;
  }

  getDisplayName() {
    return this.name + '(' + this.id + ')';
  }
}

const user = new User(1, '李四');
const json = JSON.stringify(user);
const parsed = JSON.parse(json);

console.log(parsed instanceof User); // false
console.log(typeof parsed.getDisplayName); // undefined

与原型链丢失类似的问题也出现在内置类型上。Date 会被序列化成时间字符串,但字符串再通过 parse 还原后并不会变回 Date 实例。Map 和 Set 更极端,它们的内部存储结构是不可枚举的,默认序列化结果就是一对空的花括号。如果项目里用 Map 存储状态缓存,直接 JSON.stringify 后缓存逻辑实际上被完全清空,而当你把它存进 localStorage 再读取时,可能很难意识到这个静默的数据损坏过程。

const map = new Map([['env', 'production'], ['port', 8080]]);
const set = new Set([1, 2, 3, 4]);

console.log(JSON.stringify({ map, set }));
// 输出:{"map":{},"set":{}}

console.log(JSON.parse('{"id":9223372036854775807}').id);
// 输出:9223372036854776000,精度已经丢失

数字精度问题同样值得注意,很多后端返回的雪花 ID 都超过了 JavaScript 安全整数的范围。JSON.parse 处理这类大数时会直接按 IEEE 754 浮点数规则截断,返回一个不精确的近似值。因此,在高精度场景下,通常建议后端把大 ID 转换为字符串返回,或者前端改用支持任意精度的解析器来读取原始文本数据。

四、通过 toJSON 和 replacer 定制序列化行为

JSON.stringify 的第二个参数 replacer 和对象上的 toJSON 方法为上述问题提供了官方层面的扩展入口。replacer 可以是一个函数,它在序列化过程中被逐个属性调用,开发者可以根据 key 和 value 决定是跳过某个字段、替换值还是把 Map 这类对象转换成数组。这样能够在全局层面拦截特殊类型,避免每一个调用点都写一遍额外的转换逻辑。

const data = {
  title: '项目配置',
  keywords: new Set(['vite', 'ts', 'vue']),
  createdAt: new Date('2025-03-01T10:00:00.000Z'),
};

const replacer = (key, value) => {
  if (value instanceof Set) {
    return Array.from(value);
  }
  return value;
};

const json = JSON.stringify(data, replacer, 2);
console.log(json);
// 输出中 keywords 变成了数组,createdAt 保留了字符串格式

toJSON 方法则是为单个对象定制序列化结果的更简洁方案。只要目标对象实现了 toJSON,JSON.stringify 就会优先调用它,并将返回值作为序列化依据。这使得自定义类可以在内部管理自己的状态导出格式,例如把价格单位从分转成元,或者把内部的 Buffer 转成 base64 字符串,调用方无需关心对象内部结构。

class UserSession {
  constructor(userId, token, expiresAt) {
    this.userId = userId;
    this.token = token;
    this.expiresAt = expiresAt;
  }

  toJSON() {
    return {
      userId: this.userId,
      token: this.token,
      expiresAt: this.expiresAt.getTime(),
    };
  }
}

const session = new UserSession(7, 'abc123', new Date('2025-12-31T23:59:59.000Z'));
console.log(JSON.stringify(session));
// 输出:{"userId":7,"token":"abc123","expiresAt":1767225599000}

不过,即使借助 replacer 和 toJSON,最终的还原阶段仍然需要手动处理。parse 操作无法自动执行 toJSON 的逆过程,开发者必须自己在 fetch 拿到响应后检查字段类型,再把时间戳转回 Date,把数组转回 Map。为了避免重复劳动,可以封装一个通用的 transform 函数,在 parse 之后对已知的特殊字段做一次深度修复。

总的来说,JSON 序列化适合轻量、扁平的普通对象,一旦涉及复杂类型、循环引用和精度要求,就需要在 JSON.stringify 之外补充额外的数据契约或转换层。先把需要序列化的数据改造成 JSON 兼容结构,再考虑持久化或传输,同时在后端协议设计时避开超过安全整数的大 ID,这样就能在享受 JSON 生态便利的同时规避掉绝大多数隐性问题。

JSON.stringifyJSON.parseJavaScript修改时间:2026-08-26 22:44:10

免责声明:​ 已尽一切努力确保本网站所含信息的准确性。网站内容多为原创整理与精心编撰,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们处理。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。