likes
comments
collection
share

你知道JSON.parse(JSON.stringify())实现深拷贝的原理是什么吗?

作者站长头像
站长
· 阅读数 6

很多场景我们都需要用到深拷贝,那么JSON.parse(JSON.stringify())绝对是我们首选之一,因为他既简单,又能解决我们浅拷贝带来的引用问题,那么你知道它是怎么实现深拷贝的吗?具体又有什么注意事项?最佳深拷贝方案是什么?接下来让我们一起探讨吧。

JSON.parse(JSON.stringify())虽然好用,但是也有一些弊端,所以并不是一种推荐用于实现深拷贝的方法,因为它有一些限制和潜在的问题。但首先,我们可以解释其工作原理。

先来看看它的两个方法stringifyparse

JSON.stringify()

JSON.stringify() 方法将一个 JavaScript 对象或值转换为一个 JSON 格式的字符串。这个过程会递归地遍历对象的所有可枚举属性,并将它们以及它们的值转换为字符串形式。如果对象包含嵌套的对象或数组,stringify 方法也会递归地处理这些嵌套结构。

重要的是要注意,不是所有的 JavaScript 值都可以被 JSON.stringify() 正确序列化。例如,函数、undefinedSymbol 值以及包含循环引用的对象都无法被转换为 JSON 字符串。

JSON.parse()

JSON.parse() 方法则执行相反的操作:它将一个 JSON 格式的字符串转换回一个 JavaScript 对象或值。如果 JSON 字符串表示一个对象或数组,parse 方法会创建一个新的对象或数组,并填充相应的属性或元素。

实现深拷贝的原理

结合使用 JSON.parse(JSON.stringify()) 可以实现深拷贝的原理在于:

  1. JSON.stringify() 将原始对象转换为一个 JSON 字符串,这个过程中会递归地复制对象的所有属性和嵌套结构。
  2. JSON.parse() 接着将这个 JSON 字符串转换回一个新的 JavaScript 对象。由于这个过程是基于字符串的,所以新创建的对象与原始对象在内存中是独立的,实现了深拷贝。

注意事项和限制

尽管 JSON.parse(JSON.stringify()) 在某些情况下可以实现深拷贝,但它并不是一种通用或完美的解决方案,因为它有以下限制:

  • 数据类型限制:一些 JavaScript 数据类型(如 DateRegExpMapSetBigIntArrayBuffer 等)在序列化过程中可能会丢失其原始类型或特定属性。例如,Date 对象会被转换为 ISO 格式的字符串。
  • 函数和 undefined:函数和 undefined 值在 JSON.stringify() 过程中会被忽略,因此无法被正确复制。
  • 循环引用:如果对象包含循环引用(即对象直接或间接地引用自己),JSON.stringify() 会抛出错误。
  • 性能:对于大型对象或具有复杂嵌套结构的对象,JSON.stringify() 和 JSON.parse() 的性能可能较差。
  • 精度问题:对于大数字(超过 Number.MAX_SAFE_INTEGER 的值),JSON.stringify() 可能会导致精度损失。

因此,虽然 JSON.parse(JSON.stringify()) 可以作为一种简单的深拷贝方法,但在处理复杂或特殊的数据结构时,建议使用专门的深拷贝库或手动实现深拷贝函数。

实现自己的深拷贝函数

那么接下来我们手撕一个深拷贝函数,首先我们要明确深拷贝的概念

深拷贝会递归复制对象及其子对象,为每一个复制的对象或数据类型创建一个新的指针和内存空间,从而确保原始对象和拷贝对象的引用地址完全独立。这样,原始数据和拷贝的数据在堆内存中都有自己独立的存储空间,任何修改只会影响到对应的对象,不会对其他对象产生影响。

明确拷贝点

  1. 数组深拷贝:如果输入是数组类型,使用map方法遍历数组中的每个元素,并递归调用decopy函数进行深拷贝。这样,数组中的每个元素都会被深拷贝,而不是简单地复制引用。
  2. 对象深拷贝:如果输入是对象类型,函数使用Object.keys方法获取对象的所有键,并使用reduce方法遍历这些键。对于每个键,函数递归调用decopy函数来深拷贝对应的值,并将结果存储在新的对象中。这样,对象的所有属性和值都会被深拷贝。
  3. 日期对象深拷贝:如果输入是日期对象,函数创建一个新的Date对象,并使用setTime方法设置与原始日期对象相同的时间戳。这样,新的日期对象将具有与原始对象相同的时间值,但它们在内存中是独立的。
  4. 正则表达式深拷贝:如果输入是正则表达式对象,函数首先获取正则表达式的模式和标志。然后,它使用这些信息创建一个新的RegExp对象。这样,新的正则表达式对象将具有与原始对象相同的模式和标志,但它们在内存中是独立的。
  5. 其他类型:对于其他类型的数据(如数字、字符串、布尔值等),函数直接返回原始值,因为这些类型的数据是不可变的,所以不需要进行深拷贝。

代码如下:

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

typeOf函数详情请见Github

需要注意的是,我这里并没有处理循环引用的情况。如果输入的对象中存在循环引用,这个函数可能会导致无限递归或栈溢出错误。此外,也没有处理一些特殊的数据类型,如MapSetArrayBuffer等。

如果需要处理这些复杂情况,可能需要使用更复杂的深拷贝实现或使用专门的深拷贝库。