本文是关于 JSON.parse(JSON.stringify(obj))
深拷贝方法的讨论,涵盖它的工作原理、优点、详细的缺点以及更好的替代方案。
JSON.parse(JSON.stringify(obj))
:一种有局限性的深拷贝方法
JSON.parse(JSON.stringify(obj))
实现深拷贝的方式虽然看起来简单,但在很多情况下不够好 ,并且存在明显的局限性 和潜在风险。
它是如何工作的?
这个方法利用了 JSON 的两个核心操作:
JSON.stringify(obj)
: 将 JavaScript 对象obj
转换为一个 JSON 格式的字符串。这个过程会递归地处理对象内部的属性。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 的格式规范,这就导致了信息丢失或类型变化。
以下是具体类型在转换过程中的变化:
-
undefined
(未定义值)-
丢失场景: 当
undefined
作为对象 的属性值时,在JSON.stringify()
过程中,该键值对会完全丢失。 -
数组场景: 当
undefined
作为数组 的元素时,它会被转换成null
。 -
直接转换:
JSON.stringify(undefined)
本身会返回undefined
(而不是字符串"undefined"
),这使得JSON.parse(JSON.stringify(undefined))
会直接报错,因为undefined
不是有效的 JSON。 -
影响: 如果你的原始对象依赖于某个属性存在但值为
undefined
这种状态,拷贝后的对象会失去这个属性,可能导致后续逻辑错误。 -
示例:
javascriptconst 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
-
-
Function
(函数)-
丢失: 函数不是 JSON 支持的数据类型。当
JSON.stringify()
遇到函数类型的属性值时,会完全忽略该键值对。 -
影响: 如果你的对象包含方法(函数),这些方法在拷贝后的对象中会丢失。拷贝后的对象只是一个纯数据对象。
-
示例:
javascriptconst 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
-
-
Date
对象 (日期)-
转换:
Date
对象在JSON.stringify()
时会自动调用其toJSON()
方法 ,这个方法返回一个表示该日期的 ISO 8601 格式的字符串 (例如:"2023-10-27T12:30:00.000Z"
)。 -
不恢复:
JSON.parse()
在解析这个字符串时,不会 自动将其转换回Date
对象。它仍然是一个字符串。 -
影响: 拷贝后的对象中,原本是
Date
对象的属性现在变成了字符串,如果你需要对其进行日期运算,会出错。 -
示例:
javascriptconst 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
-
-
RegExp
(正则表达式)-
转换: 正则表达式对象在
JSON.stringify()
时会被转换成一个空对象{}
。 -
影响: 拷贝后的对象丢失了正则表达式的模式和标志,无法再用于匹配。
-
示例:
javascriptconst 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); // 无法按预期工作
-
-
Map
,Set
(集合类型)-
转换:
Map
和Set
这两种 ES6 新增的数据结构,在JSON.stringify()
时同样会被转换成空对象{}
。它们包含的元素会全部丢失。 -
影响: 无法正确拷贝
Map
和Set
结构。 -
示例:
javascriptconst 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 的内容丢失
-
-
NaN
,Infinity
,-Infinity
(特殊数值)-
转换: JavaScript 中的特殊数值
NaN
(Not a Number)、Infinity
(正无穷) 和-Infinity
(负无穷) 在JSON.stringify()
时会被转换成null
。 -
影响: 拷贝后的对象丢失了这些特殊的数值信息,变成了
null
。 -
示例:
javascriptconst 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: 无法处理循环引用
循环引用是指对象内部的属性直接或间接引用了对象自身。
-
例如:
javascriptconst obj = { name: 'A' }; obj.self = obj; // obj 引用了自身,形成循环
或者
javascriptconst 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);
- 优点: 内置于现代浏览器和 Node.js (v17+),性能较好,支持更多数据类型(
- Lodash
_.cloneDeep()
:- 优点: 功能非常强大和成熟,由广泛使用的库提供,对各种数据类型和边界情况(包括循环引用)处理得很好。
- 缺点: 需要引入 Lodash 库。
- 用法:
import _ from 'lodash'; let form = _.cloneDeep(obj);
总结:
JSON.parse(JSON.stringify())
是一个看似简单实则充满陷阱的深拷贝"快捷方式"。它只适用于非常受限的、只包含 JSON 安全类型的、没有特殊对象(如 Date, RegExp)且无循环引用的简单数据结构。在大多数需要可靠深拷贝的场景下,强烈推荐使用 structuredClone()
或 Lodash 的 _.cloneDeep()
,并始终记得在使用拷贝后的对象属性前进行必要的检查(如空值检查或使用可选链 ?.
)。