js深浅拷贝

236 阅读6分钟

深拷贝和浅拷贝:

  • 浅拷贝:只复制指向对象的指针,而不复制对象本身,新旧对象共享一块内存;
    对象只会被克隆最外部的一层,至于更深层的对象,则依然是通过引用指向同一块堆内存。

  • 深拷贝:复制并创建一个一模一样的对象,不共享内存,修改新对象,旧对象保持不变。

  • 如何区分深拷贝与浅拷贝

简单点来说:

就是假设B复制了A,当修改A时,看B是否会发生变化,

如果B也跟着变了,说明这是浅拷贝,修改一个数据会影响另外一个数据。

如果B没变,那就是深拷贝。

浅拷贝拷贝一层,深拷贝拷贝多层。

实现浅拷贝的方法:

  1. Object.assign(target, source),当对象中只有一级属性,没有二级属性的时候,此方法为深拷贝,但是对象中有对象的时候,此方法,在二级属性以后就是浅拷贝。
Object.assign(target, ...sources) 【target:目标对象】,【souce:源对象(可多个)】

如果目标对象中的属性具有相同的键,则属性将被源对象中的属性覆盖;
后面的源对象的属性将类似地覆盖前面的源对象的属性。

使用例子:
const object1 = {
  a: 1,
  b: 2,
  c: 3
}

const object2 = Object.assign({c: 4, d: 5}, object1)

console.log(object2.c, object2.d)
console.log(object1)  // { a: 1, b: 2, c: 3 }
console.log(object2)  // { c: 3, d: 5, a: 1, b: 2 }

新增编辑弹窗:
aaa:默认值
mounted:this.form = Object.assign({ aaa: 1 }, this.editData)
  1. es6对象扩展运算符 let obj2 = { ...obj }

  2. Object.create()
    let newObj = Object.create(obj)
    原对象被赋值到 newObj 的 proto 上去了

  3. 循环

function shallowClone(o) {
  const obj = {};
  for (let i in o) {
    obj[i] = o[i];
  }
  return obj;
}

5. 解构赋值

  • 基本数据类型:直接复制值

说明:基本类型(numberstringboolean 等)直接复制值,新旧变量互不影响。

const obj = { a: 1, b: 'hello' };
const { a, b } = obj;

a = 2;  // 修改解构后的变量
console.log(obj.a); // 1(原对象未受影响)
  1. 引用数据类型:复制引用

说明:引用类型(对象、数组等)复制的是内存地址的引用,修改解构后的变量会直接影响原对象。

const obj = { 
  details: { name: 'Alice', hobbies: ['reading'] }
};
const { details } = obj;

details.name = 'Bob';          // 修改解构后的对象属性
details.hobbies.push('gaming');// 修改解构后的数组

console.log(obj.details.name);   // 'Bob'(原对象被修改)
console.log(obj.details.hobbies); // ['reading', 'gaming'](原数组被修改)

实现深拷贝的方法:

1、JSON.parse(JSON.stringify(obj))  (弊端:正则函数、日期会有问题,变成空对象或字符串;无法解决循环引用的问题,可能导致无限递归,出现系统栈溢出)

2、使用递归的方式

function deepClone(obj) {
  // 进行深拷贝的不能为空,并且是对象
  if (!obj || typeof obj !== 'object') return
  //判断拷贝的要进行深拷贝的是数组还是对象,是数组的话进行数组拷贝,对象的话进行对象拷贝
  let newObj = Array.isArray(obj) ? [] : {}
  for (let key in obj) {
    // Object.hasOwnProperty判断对象是否有某个特定的属性,过滤原型属性
    // 因为:原型属性通常为共享方法(如Object.prototype.toString)复制原型属性会导致冗余数据 
    // 这是深拷贝实现中非常重要的边界控制条件
    if (obj.hasOwnProperty(key)) {
      newObj[key] = typeof obj[key] === 'object' ? 
        deepClone(obj[key]) : obj[key]
    }
  }
  return newObj
}

3、在生产环境中使用函数库lodash-es的cloneDeep()

import { debounce, throttle, cloneDeep } from 'lodash-es';

4、通过jQuery的$.extend()实现深拷贝,当extend内的第一个参数为true时,实现的是深拷贝,false是浅拷贝。

5、Object.assign(target, source),当对象中只有一级属性,没有二级属性的时候,此方法为深拷贝,但是对象中有对象的时候,此方法,在二级属性以后就是浅拷贝。

手写实现

/**
 * 深拷贝函数(处理循环引用和特殊对象类型)
 * @param {any} value - 需要拷贝的值
 * @param {WeakMap} hash - 弱映射表用于存储已拷贝对象(解决循环引用)
 * @returns {any} 深拷贝后的新对象
 */
function deepClone(value, hash = new WeakMap()) {
  // value - 需要拷贝的值;hash - 弱映射表用于存储已拷贝对象(解决循环引用)
  // 选择WeakMap而不是Map,因为WeakMap的键是弱引用,不会造成内存泄漏,适合这种临时存储的场景
  // 基础类型直接返回(包括null、undefined、string、number、boolean、symbol等)
  if (value === null || typeof value !== 'object') {
    return value;
  }

  // 处理 Date 类型(创建新的Date实例)
  if (value instanceof Date) {
    return new Date(value); // 拷贝日期对象的时间戳
  }

  // 处理 RegExp 类型(创建新的正则表达式)
  if (value instanceof RegExp) {
    return new RegExp(value); // 保留源正则的表达式和标志
  }

  // 处理数组类型(递归拷贝每个元素)
  if (Array.isArray(value)) {
    const arrCopy = [];
    for (let item of value) {
      // 递归拷贝数组元素,并传递hash表
      arrCopy.push(deepClone(item, hash));
    }
    return arrCopy;
  }

  // 检查循环引用(如果已经拷贝过则直接返回已存储的副本)
  if (hash.has(value)) {
    return hash.get(value); // 避免无限递归的关键
  }

  // 创建空对象副本,并存入hash表(必须在递归前存入)
  const objCopy = {};
  hash.set(value, objCopy);

  // 遍历对象自身可枚举属性
  for (let key in value) {
    // 检查对象属性是否为该对象自身拥有的属性​(而非从原型链继承的属性)过滤原型属性,不拷贝原型链上的属性
    // 因为:原型属性通常为共享方法(如Object.prototype.toString)复制原型属性会导致冗余数据
    // 这是深拷贝实现中非常重要的边界控制条件
    if (value.hasOwnProperty(key)) {
      // 递归拷贝每个属性值
      objCopy[key] = deepClone(value[key], hash);
    }
  }

  return objCopy;
}
// 执行流程说明:
// 1. 基础类型直接返回
// 2. 特殊对象类型(Date / RegExp) 创建新实例
// 3. 数组类型递归拷贝元素
// 4. 普通对象处理:
    // a.检查循环引用,有则直接返回副本
    // b.创建空对象,存入hash表
    // c.递归拷贝每个自身属性
    // d.返回拷贝后的新对象

深拷贝怎么解决循环引用的问题

参考解决方式一:使用WeakMap:

解决循环引用问题,我们可以开辟一个存储空间,来存储当前对象和拷贝对象的对应关系,

这个存储空间,需要可以存储key-value形式的数据,且key是一个引用类型。

我们可以选WeakMap这种数据结构:

检查WeakMap中有无克隆过的对象,有,直接返回,没有,将当前对象作为key,克隆对象作为value进行存储,继续克隆。

参考解决方式二: 可以用Set,发现相同的对象直接赋值,也可用Map

解决方法应该是 用一个 Map 来存储引用类型,然后每次遇到引用属性时,就用 has 查看是否已经有了这个引用。

深拷贝如何拷贝函数

toString()方法

深拷贝.jpg