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 (集合类型)

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

    • 影响: 无法正确拷贝 Map 和 Set 结构。

    • 示例:

      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() ,并始终记得在使用拷贝后的对象属性前进行必要的检查(如空值检查或使用可选链 ?.)。

相关推荐
code2cat8 小时前
【随笔】结构化输出:JSON格式过关,业务数据就能入库吗
json·大语言模型
卸任11 小时前
Vibe coding工具使用心得
前端·ai编程
brien912 小时前
为什么模型标称 1M,Google Antigravity 聊到 256K 却突然失忆?我们给它做了个原生行内监控圆环
前端
修炼的dance12 小时前
录屏关键帧间隔多少合适?六档实测,2秒到10秒小了75.6%
前端
Lank_M12 小时前
关键帧从1秒拉到10秒,录屏小87.3%,摄像头小10.8%
前端
补码缩写12 小时前
用 ffprobe 拆录屏,八成字节在 29 个关键帧上
前端
百思不得小李12 小时前
一个复杂的动态表单是如何实现的
前端·设计模式
陈随易13 小时前
Cmder 停更 17 个月,2.0 支持 Windows Terminal
前端·后端·程序员
风骏时光牛马13 小时前
前沿视界
前端
kyriewen13 小时前
我扫了 814 个依赖,97 个还敢要 Node 10
前端·javascript·程序员