很多场景我们都需要用到深拷贝,那么JSON.parse(JSON.stringify())
绝对是我们首选之一,因为他既简单,又能解决我们浅拷贝带来的引用问题,那么你知道它是怎么实现深拷贝的吗?具体又有什么注意事项?最佳深拷贝方案是什么?接下来让我们一起探讨吧。
JSON.parse(JSON.stringify())
虽然好用,但是也有一些弊端,所以并不是一种推荐用于实现深拷贝的方法,因为它有一些限制和潜在的问题。但首先,我们可以解释其工作原理。
先来看看它的两个方法stringify ,parse
JSON.stringify()
JSON.stringify()
方法将一个 JavaScript 对象或值转换为一个 JSON 格式的字符串。这个过程会递归地遍历对象的所有可枚举属性,并将它们以及它们的值转换为字符串形式。如果对象包含嵌套的对象或数组,stringify
方法也会递归地处理这些嵌套结构。
重要的是要注意,不是所有的 JavaScript 值都可以被 JSON.stringify()
正确序列化。例如,函数、undefined
、Symbol
值以及包含循环引用的对象都无法被转换为 JSON 字符串。
JSON.parse()
JSON.parse()
方法则执行相反的操作:它将一个 JSON 格式的字符串转换回一个 JavaScript 对象或值。如果 JSON 字符串表示一个对象或数组,parse
方法会创建一个新的对象或数组,并填充相应的属性或元素。
实现深拷贝的原理
结合使用 JSON.parse(JSON.stringify())
可以实现深拷贝的原理在于:
JSON.stringify()
将原始对象转换为一个 JSON 字符串,这个过程中会递归地复制对象的所有属性和嵌套结构。JSON.parse()
接着将这个 JSON 字符串转换回一个新的 JavaScript 对象。由于这个过程是基于字符串的,所以新创建的对象与原始对象在内存中是独立的,实现了深拷贝。
注意事项和限制
尽管 JSON.parse(JSON.stringify())
在某些情况下可以实现深拷贝,但它并不是一种通用或完美的解决方案,因为它有以下限制:
- 数据类型限制 :一些 JavaScript 数据类型(如
Date
、RegExp
、Map
、Set
、BigInt
、ArrayBuffer
等)在序列化过程中可能会丢失其原始类型或特定属性。例如,Date
对象会被转换为 ISO 格式的字符串。 - 函数和
undefined
:函数和undefined
值在JSON.stringify()
过程中会被忽略,因此无法被正确复制。 - 循环引用 :如果对象包含循环引用(即对象直接或间接地引用自己),
JSON.stringify()
会抛出错误。 - 性能 :对于大型对象或具有复杂嵌套结构的对象,
JSON.stringify()
和JSON.parse()
的性能可能较差。 - 精度问题 :对于大数字(超过
Number.MAX_SAFE_INTEGER
的值),JSON.stringify()
可能会导致精度损失。
因此,虽然 JSON.parse(JSON.stringify())
可以作为一种简单的深拷贝方法,但在处理复杂或特殊的数据结构时,建议使用专门的深拷贝库或手动实现深拷贝函数。
实现自己的深拷贝函数
那么接下来我们手撕一个深拷贝函数,首先我们要明确深拷贝的概念
深拷贝会递归复制对象及其子对象,为每一个复制的对象或数据类型创建一个新的指针和内存空间,从而确保原始对象和拷贝对象的引用地址完全独立。这样,原始数据和拷贝的数据在堆内存中都有自己独立的存储空间,任何修改只会影响到对应的对象,不会对其他对象产生影响。
明确拷贝点
- 数组深拷贝 :如果输入是数组类型,使用
map
方法遍历数组中的每个元素,并递归调用decopy
函数进行深拷贝。这样,数组中的每个元素都会被深拷贝,而不是简单地复制引用。 - 对象深拷贝 :如果输入是对象类型,函数使用
Object.keys
方法获取对象的所有键,并使用reduce
方法遍历这些键。对于每个键,函数递归调用decopy
函数来深拷贝对应的值,并将结果存储在新的对象中。这样,对象的所有属性和值都会被深拷贝。 - 日期对象深拷贝 :如果输入是日期对象,函数创建一个新的
Date
对象,并使用setTime
方法设置与原始日期对象相同的时间戳。这样,新的日期对象将具有与原始对象相同的时间值,但它们在内存中是独立的。 - 正则表达式深拷贝 :如果输入是正则表达式对象,函数首先获取正则表达式的模式和标志。然后,它使用这些信息创建一个新的
RegExp
对象。这样,新的正则表达式对象将具有与原始对象相同的模式和标志,但它们在内存中是独立的。 - 其他类型:对于其他类型的数据(如数字、字符串、布尔值等),函数直接返回原始值,因为这些类型的数据是不可变的,所以不需要进行深拷贝。
代码如下:
typescript
import { typeOf } from "js-hodgepodge" // 引入js-hodgepodge的精确类型判断
// 深拷贝
export function decopy<T>(data: T): T {
let val = data as any
switch (typeOf(val)) {
case 'array':
return val.map((c: any) => decopy(c))
case 'object':
return Object.keys(val).reduce((ret: any, key: string) => {
ret[key] = decopy(val[key])
return ret
}, {})
case 'date':
const newDate = new Date()
newDate.setTime(val.getTime())
return newDate as any
case 'regExp':
let pattern = val.valueOf()
let flags = ''
flags += pattern.global ? 'g' : '';
flags += pattern.ignoreCase ? 'i' : '';
flags += pattern.multiline ? 'm' : '';
return new RegExp(pattern.source, flags) as any
default:
return val
}
}
export default decopy
需要注意的是,我这里并没有处理循环引用的情况。如果输入的对象中存在循环引用,这个函数可能会导致无限递归或栈溢出错误。此外,也没有处理一些特殊的数据类型,如Map
、Set
、ArrayBuffer
等。
如果需要处理这些复杂情况,可能需要使用更复杂的深拷贝实现或使用专门的深拷贝库。