JSON.parse(JSON.stringify()) 的“坑”:数据丢失与类型转换问题

本文是关于 JSON.parse(JSON.stringify(obj)) 深拷贝方法的讨论,涵盖它的工作原理、优点、详细的缺点以及更好的替代方案。


JSON.parse(JSON.stringify(obj)):一种有局限性的深拷贝方法

JSON.parse(JSON.stringify(obj)) 实现深拷贝的方式虽然看起来简单,但在很多情况下不够好 ,并且存在明显的局限性潜在风险

它是如何工作的?

这个方法利用了 JSON 的两个核心操作:

  1. JSON.stringify(obj): 将 JavaScript 对象 obj 转换为一个 JSON 格式的字符串。这个过程会递归地处理对象内部的属性。
  2. JSON.parse(...): 将上一步生成的 JSON 字符串解析(反序列化)成一个新的 JavaScript 对象。

由于数据经过了"对象 -> 字符串 -> 对象"的转换,新生成的对象与原始对象在内存中是完全独立的副本,修改新对象不会影响旧对象,从而实现了深拷贝。


优点:

  • 写法简单: 代码非常简洁,容易理解其意图。
  • 无需外部库: 不依赖像 Lodash 这样的第三方库,是 JavaScript 的原生功能。
  • 适用于纯粹的 JSON 安全数据: 对于只包含字符串、数字、布尔值、null 以及由这些类型构成的嵌套对象和数组的数据结构,它能很好地工作。

缺点:

它的主要问题在于 JSON 本身的局限性以及序列化/反序列化的过程特性:

我们来详细讲解一下 JSON.parse(JSON.stringify(obj)) 深拷贝方法的三个主要缺点。

缺点 1: 数据类型丢失或转换 (最核心的问题)

JSON (JavaScript Object Notation) 设计的初衷是作为一种轻量级的数据交换格式。它的类型系统比 JavaScript 的类型系统要简单得多JSON.stringify() 的作用是将 JavaScript 值转换为 JSON 字符串,这个过程必须遵守 JSON 的格式规范,这就导致了信息丢失或类型变化。

以下是具体类型在转换过程中的变化:

  1. undefined (未定义值)

    • 丢失场景:undefined 作为对象 的属性值时,在 JSON.stringify() 过程中,该键值对会完全丢失

    • 数组场景:undefined 作为数组 的元素时,它会被转换成 null

    • 直接转换: JSON.stringify(undefined) 本身会返回 undefined (而不是字符串 "undefined"),这使得 JSON.parse(JSON.stringify(undefined)) 会直接报错,因为 undefined 不是有效的 JSON。

    • 影响: 如果你的原始对象依赖于某个属性存在但值为 undefined 这种状态,拷贝后的对象会失去这个属性,可能导致后续逻辑错误。

    • 示例:

      javascript 复制代码
      const obj = { a: 1, b: undefined, c: [10, undefined, 20] };
      const jsonString = JSON.stringify(obj); // '{"a":1,"c":[10,null,20]}'
      const copiedObj = JSON.parse(jsonString);
      console.log(copiedObj); // 输出: { a: 1, c: [ 10, null, 20 ] }
      // 注意:属性 'b' 消失了,数组中的 undefined 变成了 null
      console.log(copiedObj.hasOwnProperty('b')); // false
  2. Function (函数)

    • 丢失: 函数不是 JSON 支持的数据类型。当 JSON.stringify() 遇到函数类型的属性值时,会完全忽略该键值对。

    • 影响: 如果你的对象包含方法(函数),这些方法在拷贝后的对象中会丢失。拷贝后的对象只是一个纯数据对象。

    • 示例:

      javascript 复制代码
      const obj = {
        name: "Example",
        sayHello: function() { console.log("Hello!"); },
        value: 10
      };
      const jsonString = JSON.stringify(obj); // '{"name":"Example","value":10}'
      const copiedObj = JSON.parse(jsonString);
      console.log(copiedObj); // 输出: { name: 'Example', value: 10 }
      // 'sayHello' 方法丢失了
      // copiedObj.sayHello(); // 这会报错 TypeError: copiedObj.sayHello is not a function
  3. Date 对象 (日期)

    • 转换: Date 对象在 JSON.stringify() 时会自动调用其 toJSON() 方法 ,这个方法返回一个表示该日期的 ISO 8601 格式的字符串 (例如: "2023-10-27T12:30:00.000Z")。

    • 不恢复: JSON.parse() 在解析这个字符串时,不会 自动将其转换回 Date 对象。它仍然是一个字符串。

    • 影响: 拷贝后的对象中,原本是 Date 对象的属性现在变成了字符串,如果你需要对其进行日期运算,会出错。

    • 示例:

      javascript 复制代码
      const obj = { eventName: "Meeting", time: new Date() };
      const jsonString = JSON.stringify(obj); // '{"eventName":"Meeting","time":"2023-10-27T...Z"}' (具体时间字符串)
      const copiedObj = JSON.parse(jsonString);
      console.log(copiedObj); // 输出: { eventName: 'Meeting', time: '2023-10-27T...Z' }
      console.log(typeof copiedObj.time); // 'string'
      // copiedObj.time.getFullYear(); // 报错 TypeError: copiedObj.time.getFullYear is not a function
  4. RegExp (正则表达式)

    • 转换: 正则表达式对象在 JSON.stringify() 时会被转换成一个空对象 {}

    • 影响: 拷贝后的对象丢失了正则表达式的模式和标志,无法再用于匹配。

    • 示例:

      javascript 复制代码
      const obj = { pattern: /ab+c/i, value: 5 };
      const jsonString = JSON.stringify(obj); // '{"pattern":{},"value":5}'
      const copiedObj = JSON.parse(jsonString);
      console.log(copiedObj); // 输出: { pattern: {}, value: 5 }
      // 正则表达式信息丢失
      // "abc".match(copiedObj.pattern); // 无法按预期工作
  5. Map, Set (集合类型)

    • 转换: MapSet 这两种 ES6 新增的数据结构,在 JSON.stringify() 时同样会被转换成空对象 {}。它们包含的元素会全部丢失。

    • 影响: 无法正确拷贝 MapSet 结构。

    • 示例:

      javascript 复制代码
      const obj = {
        myMap: new Map([['a', 1], ['b', 2]]),
        mySet: new Set([1, 2, 3])
      };
      const jsonString = JSON.stringify(obj); // '{"myMap":{},"mySet":{}}'
      const copiedObj = JSON.parse(jsonString);
      console.log(copiedObj); // 输出: { myMap: {}, mySet: {} }
      // Map 和 Set 的内容丢失
  6. NaN, Infinity, -Infinity (特殊数值)

    • 转换: JavaScript 中的特殊数值 NaN (Not a Number)、Infinity (正无穷) 和 -Infinity (负无穷) 在 JSON.stringify() 时会被转换成 null

    • 影响: 拷贝后的对象丢失了这些特殊的数值信息,变成了 null

    • 示例:

      javascript 复制代码
      const obj = { a: NaN, b: Infinity, c: -Infinity, d: 10 };
      const jsonString = JSON.stringify(obj); // '{"a":null,"b":null,"c":null,"d":10}'
      const copiedObj = JSON.parse(jsonString);
      console.log(copiedObj); // 输出: { a: null, b: null, c: null, d: 10 }

缺点 2: 性能问题

虽然 JSON.parse(JSON.stringify()) 写起来简单,但在处理大型或层级很深的对象时,它的性能可能不是最优的。

  • 序列化开销: JSON.stringify() 需要遍历整个对象(包括所有嵌套的对象和数组),并将其转换成一个长字符串。这个过程涉及类型检查、字符串拼接等操作,会消耗 CPU 和内存。
  • 反序列化开销: JSON.parse() 需要读取这个长字符串,根据 JSON 语法规则解析它,并重新构建 JavaScript 对象。这个过程同样需要消耗计算资源。

对于非常大的数据结构,这个"先变字符串再变回来"的过程可能比专门优化的深拷贝库(如 Lodash 的 _.cloneDeep)或原生 API(如 structuredClone)要慢。虽然对于中小型对象差异不明显,但在性能敏感的场景下需要考虑。


缺点 3: 无法处理循环引用

循环引用是指对象内部的属性直接或间接引用了对象自身。

  • 例如:

    javascript 复制代码
    const obj = { name: 'A' };
    obj.self = obj; // obj 引用了自身,形成循环

    或者

    javascript 复制代码
    const a = { name: 'A' };
    const b = { name: 'B' };
    a.link = b;
    b.link = a; // a 和 b 相互引用,形成循环
  • JSON.stringify() 的行为:JSON.stringify() 检测到循环引用时,它无法将这个无限结构转换成有限的 JSON 字符串,因此会直接抛出 TypeError 错误 (通常是类似 "Converting circular structure to JSON" 的错误信息)。

  • 影响: 如果你的数据结构中可能存在循环引用(这在复杂应用中并不罕见,例如双向链表、某些树结构或 DOM 对象),使用 JSON.parse(JSON.stringify()) 会直接导致程序崩溃。


选择更优的深拷贝方法:

  • structuredClone() (现代首选):
    • 优点: 内置于现代浏览器和 Node.js (v17+),性能较好,支持更多数据类型(Date, RegExp, Map, Set, Blob, File, ArrayBuffer 等),能处理循环引用。
    • 缺点: 仍不支持拷贝函数 (Function),会丢失原型链。
    • 用法: let form = structuredClone(obj);
  • Lodash _.cloneDeep():
    • 优点: 功能非常强大和成熟,由广泛使用的库提供,对各种数据类型和边界情况(包括循环引用)处理得很好。
    • 缺点: 需要引入 Lodash 库。
    • 用法: import _ from 'lodash'; let form = _.cloneDeep(obj);

总结:

JSON.parse(JSON.stringify()) 是一个看似简单实则充满陷阱的深拷贝"快捷方式"。它只适用于非常受限的、只包含 JSON 安全类型的、没有特殊对象(如 Date, RegExp)且无循环引用的简单数据结构。在大多数需要可靠深拷贝的场景下,强烈推荐使用 structuredClone() 或 Lodash 的 _.cloneDeep() ,并始终记得在使用拷贝后的对象属性前进行必要的检查(如空值检查或使用可选链 ?.)。

相关推荐
朴拙数科18 分钟前
MCP(模型上下文协议)、A2A(Agent2Agent)协议和JSON-RPC 2.0的前沿技术解析
网络协议·rpc·json
2401_878454531 小时前
Themeleaf复用功能
前端·学习
葡萄城技术团队3 小时前
基于前端技术的QR码API开发实战:从原理到部署
前端
八了个戒4 小时前
「数据可视化 D3系列」入门第三章:深入理解 Update-Enter-Exit 模式
开发语言·前端·javascript·数据可视化
noravinsc5 小时前
html页面打开后中文乱码
前端·html
小满zs5 小时前
React-router v7 第四章(路由传参)
前端·react.js
小陈同学呦5 小时前
聊聊双列瀑布流
前端·javascript·面试
键指江湖6 小时前
React 在组件间共享状态
前端·javascript·react.js
诸葛亮的芭蕉扇6 小时前
D3路网图技术文档
前端·javascript·vue.js·microsoft
小离a_a6 小时前
小程序css实现容器内 数据滚动 无缝衔接 点击暂停
前端·css·小程序