前端必看!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
}

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

相关推荐
雯0609~26 分钟前
js:循环查询数组对象中的某一项的值是否为空
开发语言·前端·javascript
bingbingyihao32 分钟前
个人博客系统
前端·javascript·vue.js
天天扭码1 小时前
一杯咖啡的时间吃透一道算法题——2.两数相加(使用链表)
前端·javascript·算法
NetX行者1 小时前
详解正则表达式中的?:、?= 、 ?! 、?<=、?<!
开发语言·前端·javascript·正则表达式
vvilkim2 小时前
React 高级特性与最佳实践
前端·javascript·react.js
拉不动的猪2 小时前
vue与react中监听的简单对比
前端·javascript·面试
涵信2 小时前
第七节:React HooksReact 18+新特性-并发模式(Concurrent Mode)解决了什么问题?
前端·javascript·react.js
冴羽2 小时前
SvelteKit 最新中文文档教程(21)—— 最佳实践之图片
前端·javascript·svelte
纪元A梦2 小时前
华为OD机试真题——跳格子3(2025A卷:200分)Java/python/JavaScript/C++/C语言/GO六种最佳实现
java·javascript·c++·python·华为od·go·华为od机试题
BillKu2 小时前
遵守 Vue3 的单向数据流原则:父组件传递对象 + 子组件修改对象属性,安全地实现父子组件之间复杂对象的双向绑定示例代码及讲解
javascript·vue.js·elementui