前端必看!JSON.parse(JSON.stringify())竟不是万能解?深拷贝的黑暗森林法则

在 JavaScript 中,深拷贝和浅拷贝是处理对象和数组复制时的两种重要概念,它们的主要区别在于复制对象内部嵌套对象或数组时的行为。

浅拷贝

  1. 定义:浅拷贝会创建一个新的对象或数组,这个新的对象或数组会复制原对象或数组的基本类型值和引用类型的引用地址。也就是说,对于引用类型(如对象、数组),新对象和原对象会共享这些引用,对其中一个对象中引用类型值的修改会反映到另一个对象上。

  2. 实现方式

  • 对象的浅拷贝 :可以使用Object.assign()方法。它将所有可枚举属性的值从一个或多个源对象复制到目标对象。例如:
js 复制代码
let originalObject = { a: 1, b: { c: 2 } };
let shallowCopiedObject = Object.assign({}, originalObject);
console.log(shallowCopiedObject); 
// { a: 1, b: { c: 2 } }

// 修改浅拷贝对象中的嵌套对象
shallowCopiedObject.b.c = 3;
console.log(originalObject.b.c); 
// 3,原对象也被改变,因为共享引用
  • 数组的浅拷贝 :可以使用数组的slice()方法或concat()方法。slice()方法会返回一个新的数组对象,这一对象是一个由beginend决定的原数组的浅拷贝(包括begin,不包括end)。concat()方法用于合并两个或多个数组,返回一个新数组,也是浅拷贝。例如:
js 复制代码
let originalArray = [1, { value: 2 }];
let slicedArray = originalArray.slice();
let concatenatedArray = [].concat(originalArray);

// 修改浅拷贝数组中的嵌套对象
slicedArray[1].value = 3;
console.log(originalArray[1].value); 
// 3,原数组也被改变,因为共享引用

深拷贝

  1. 定义:深拷贝会递归地复制对象及其所有嵌套的对象和数组,创建一个与原对象完全独立的新对象。对新对象的任何修改都不会影响原对象,反之亦然。

  2. 实现方式

  • 使用JSON.parse(JSON.stringify()) :这是一种简单但有局限性的深拷贝方法。它先将对象转换为 JSON 字符串,再将其解析回对象,从而实现深拷贝。例如:
js 复制代码
let original = { a: 1, b: { c: 2 } };
let deepCopied = JSON.parse(JSON.stringify(original));

// 修改深拷贝对象中的嵌套对象
deepCopied.b.c = 3;
console.log(original.b.c); 
// 2,原对象不受影响
  • 局限性 :这种方法不能处理函数、undefinedSymbol等特殊类型。例如:
js 复制代码
let originalWithFunction = { a: 1, func: function() { return 'Hello'; } };
let copiedWithFunction = JSON.parse(JSON.stringify(originalWithFunction));
console.log(copiedWithFunction.func); 
// undefined,函数丢失
  • 手动实现深拷贝:可以通过递归的方式手动实现深拷贝,以处理各种复杂情况。以下是一个简单的深拷贝函数示例:
js 复制代码
function deepCopy(obj) {
    if (typeof obj!== 'object' || obj === null) {
        return obj;
    }
    let copy = Array.isArray(obj)? [] : {};
    for (let key in obj) {
        if (obj.hasOwnProperty(key)) {
            copy[key] = deepCopy(obj[key]);
        }
    }
    return copy;
}

let complexObject = { a: 1, b: [2, 3], c: { d: 4 } };
let deeplyCopied = deepCopy(complexObject);

// 修改深拷贝对象中的嵌套对象
deeplyCopied.c.d = 5;
console.log(complexObject.c.d); 
// 4,原对象不受影响

在这个函数中,首先判断传入的obj是否为对象或null,如果不是则直接返回。然后根据obj是数组还是对象创建相应的空容器copy。接着通过for...in循环遍历obj的所有属性,并使用递归调用deepCopy来复制属性值,确保所有嵌套的对象和数组都被深拷贝。

  • 多方面考虑深拷贝 :考虑不同的数据类型,比如基本类型(number, string,boolean, null, undefined)、对象、数组、日期、正则表达式、MapSet等等。不同的数据类型可能需要不同的处理方式。比如,处理日期对象时,应该创建一个新的Date实例,而不是直接复制引用。然后,如何处理循环引用的问题。比如,如果一个对象的属性引用了自身,或者对象之间相互引用,普通的深拷贝方法可能会进入无限递归,导致栈溢出。这时候需要一种机制来记录已经拷贝过的对象,避免重复拷贝,可以使用WeakMap来保存已拷贝的对象,键是原对象,值是拷贝后的对象。接下来,我需要思考如何递归地处理对象的属性。对于普通对象和数组,需要遍历它们的属性或元素,并对每个属性值进行深拷贝。对于其他特殊对象类型,比如RegExpDateMapSet,需要分别处理。例如,正则表达式可以直接复制,但可能需要保留flagssourceDate对象需要创建新的实例;MapSet需要遍历其元素并进行深拷贝。以下是一个完整的深拷贝过程代码示例:
js 复制代码
function deepClone(target, map = new WeakMap()) {
  // 处理非对象类型(基本类型、函数)
  if (typeof target !== 'object' || target === null) return target

  // 处理循环引用
  if (map.has(target)) return map.get(target)

  // 处理特殊对象类型
  switch (true) {
    case target instanceof Date:
      return new Date(target)
    case target instanceof RegExp:
      return new RegExp(target.source, target.flags)
    case target instanceof Map:
      return cloneMap(target, map)
    case target instanceof Set:
      return cloneSet(target, map)
    case ArrayBuffer.isView(target):  // 处理 TypedArray
      return target.slice()
    case target instanceof ArrayBuffer: // 处理 ArrayBuffer
      return cloneArrayBuffer(target)
  }

  // 处理普通对象/数组
  const clone = Array.isArray(target) 
    ? []
    : Object.create(Object.getPrototypeOf(target))

  map.set(target, clone) // 记录已克隆对象

  // 处理 Symbol 属性
  Reflect.ownKeys(target).forEach(key => {
    clone[key] = deepClone(target[key], map)
  })

  return clone
}

深拷贝和浅拷贝各有其适用场景。浅拷贝适用于只需要复制对象的顶层结构,且不担心共享引用带来的问题时;而深拷贝则适用于需要完全独立的对象副本,避免对新对象的修改影响原对象的情况。

相关推荐
星月心城40 分钟前
Promise之什么是promise?(01)
javascript
二川bro1 小时前
第二篇:Three.js核心三要素:场景、相机、渲染器
开发语言·javascript·数码相机
Mintopia1 小时前
🧱 用三维点亮前端宇宙:构建你自己的 Three.js 组件库
前端·javascript·three.js
小西↬2 小时前
vite+vue3+websocket处理音频流发送到后端
javascript·websocket·音视频
Mintopia2 小时前
🚀 顶点-面碰撞检测之诗:用牛顿法追寻命运的交点
前端·javascript·计算机图形学
烛阴3 小时前
解锁 Gulp 的潜力:高级技巧与工作流优化
前端·javascript
Entropy-Lee3 小时前
JavaScript 语句和函数
开发语言·前端·javascript
cos5 小时前
FE Bits 前端周周谈 Vol.1|Hello World、TanStack DB 首个 Beta 版发布
前端·javascript·css
剪刀石头布啊6 小时前
var、let、const与闭包、垃圾回收
前端·javascript
剪刀石头布啊6 小时前
js常见的单例
前端·javascript