JS对象拷贝的几种实现方法以及如何深拷贝


前言

js中的对象深拷贝在项目开发中较常用到,本文介绍一下Js对象拷贝的几种实现方法,以及如何深拷贝。


一、浅拷贝与深拷贝

浅拷贝是创建一个新对象,这个对象有着原始对象属性值的拷贝。如果属性是基本类型,拷贝的就是基本类型的值,如果属性是引用类型,拷贝的是引用内存地址。可以称之为拷贝了,但没完全拷贝。

深拷贝是对象从内存中完整的拷贝一份出来,从堆内存中开辟一个新的区域来存放新对象,所以修改新对象不会影响原对象。如果不进行深拷贝,其中一个对象改变了浅拷贝所拷贝的引用值内部的属性,就会影响到另一个对象的属性值。

二、几种拷贝方式

1. JSON.parse(JSON.stringify(obj))

代码如下(示例):

let a = {
    name: 'Jack',
    age: 18,
    hobbit: ['sing', {type: 'sports', value: 'run'}],
    score: {
        math: 'A',
    },
    run: function() {}, // 无法拷贝
    walk: undefined, // 无法拷贝
    fly: NaN, // --> null
    cy: null,
    date: new Date() // date 字符串
}
let b = JSON.parse(JSON.stringify(a))

此种方式可以深拷贝一个对象,但是这个对象的内容得符合一定的限制要求,才能真正的深拷贝而没有遗漏或拷贝偏差。下面列举这个方法的缺陷。

缺陷:

  1. 取不到值为 undefined 的 key;
  2. 如果对象里有函数,函数无法被拷贝下来;
  3. 无法拷贝 copyObj 对象原型链上的属性和方法;
  4. 对象转变为 date 字符串;
  5. NaN 转变为 null。

JSON 本来就不是专门为深拷贝而设计出来的,该方法的原理就是将对象先转换为 JSON 字符串,再从 JSON 字符串解析回 JavaScript 对象。JSON 所能保存的类型有限,转换为 JSON 字符串的过程中会按照 JSON 的一些规则处理,而无法保留原对象的所有细节。所以,如果深拷贝的应用场景无法接受这些细节的丢失,则不要使用这种方式深拷贝。

2. Object.assign(target, source1, source2)

代码如下(示例):

var data = {
              a: "123",
              b: 123,
              c: true,
              d: [43, 2],
              e: undefined,
              f: null,
              g: function() {    console.log("g");  },
              h: new Set([3, 2, null]),
              i: Symbol("fsd"),
              k: new Map([    ["name", "张三"],    ["title", "Author"]  ])
        };
var newData = Object.assign({},data)

这种方式时一种浅拷贝方法。它只会复制对象的第一层属性,而不会复制对象内部的所有嵌套属性。

Object.assign方法作用是将 targetObj 和 sourceObj 合并,返回值是合并后的 targetObj 的引用,而这个过程只进行了浅拷贝。

3. 普通递归函数实现深拷贝

function deepClone(source) {
  if (typeof source !== 'object' || source == null) {
    return source;
  }
  const target = Array.isArray(source) ? [] : {};
  for (const key in source) {
    if (Object.prototype.hasOwnProperty.call(source, key)) {
      if (typeof source[key] === 'object' && source[key] !== null) {
        target[key] = deepClone(source[key]);
      } else {
        target[key] = source[key];
      }
    }
  }
  return target;
}

解决循环引用和symblo类型

function cloneDeep(source, hash = new WeakMap()) {
  if (typeof source !== 'object' || source === null) {
    return source;
  }
  if (hash.has(source)) {
    return hash.get(source);
  }
  const target = Array.isArray(source) ? [] : {};
  Reflect.ownKeys(source).forEach(key => {
    const val = source[key];
    if (typeof val === 'object' && val != null) {
      target[key] = cloneDeep(val, hash);
    } else {
      target[key] = val;
    }
  })
  return target;
}

4. 迭代递归方法(解决闭环问题)

function deepCopy(data, hash = new WeakMap()) {
  let newData;
  if (typeof data === "object") {
    // null
    if (data === null) {
      newData = data;
    }
    // Array
    else if (Array.isArray(data)) {
      newData = [];
      data.forEach((item) => newData.push(deepClone(item, hash)));
    }
    // Date
    else if (data instanceof Date) {
      newData = new Date(data);
    }
    // regular expression
    else if (data instanceof RegExp) {
      newData = new RegExp(data);
    } else if (data instanceof Set) {
      // 实现set数据的深拷贝
      newData = new Set();
      Array.from(data).forEach((item) => newData.add(deepClone(item, hash)));
    } else if (data instanceof Map) {
      // 实现map数据的深拷贝
      newData = new Map();
      data.forEach((value, key) =>
        newData.set(deepClone(key, hash), deepClone(value, hash))
      );
    }
    // plain object
    else {
      // 用WeakMap的key保存原对象的引用记录, value是对应的深拷贝对象的引用
      // 例如: a:{b:{c:{d: null}}}, d=a, a 的深拷贝对象是 copy, 则 weakmap 里保存一条 a->copy 记录
      // 当递归拷贝到d, 发现d指向a,而a已经存在于weakmap,则让新d指向copy
      if (hash.has(data)) {
        newData = hash.get(data);
      } else {
        newData = {};
        hash.set(data, newData);
        for (let prop in data) {
          newData[prop] = deepClone(data[prop], hash);
        }
      }
    }
  }
  // 基本数据类型
  else {
    newData = data;
  }
  return newData;
}

初次调用deepCopy时,参数会创建一个WeakMap结构的对象,这种数据结构的特点之一是,存储键值对中的健必须是对象类型。

如果待拷贝对象中有属性也为对象时,则将该待拷贝对象存入weakMap中,此时的健是对该待拷贝对象的引用,值是拷贝结果对象的引用。然后递归调用该函数再次进入该函数,传入了上一个待拷贝对象的对象属性的引用和存储了上一个待拷贝对象引用的weakMap,因为如果是循环引用产生的闭环,那么这两个引用是指向相同的对象的,因此会进入if(hash.has())语句内,然后直接赋值return,退出函数,所以不会一直循环递归进栈,以此防止栈溢出。


总结

上述的几种方式不管优缺点如何,共同点是只能拷贝对象的可枚举属性,对于不可枚举或者原型上的属性,却不能拷贝,但对于基本的使用来说,已经足够了。

在编程中,对象和数组的深拷贝涉及到数据结构的副本创建,确保新创建的对象与原对象独立,即使原对象发生改变也不会影响副本。这里有几种常见的深拷贝方式: 1. **浅拷贝(Shallow Copy)**: 对于简单类型(如基本数据类型)或值类型,拷贝的是它们的值。对于复杂类型如数组或对象,浅拷贝实际上是创建了一个引用,两个变量指向的是同一个内存地址。这意味着对其中一个的修改会影响到另一个。 2. **深拷贝(Deep Copy)**: - **复制数组**:对于数组,可以逐个元素地进行深拷贝,创建一个新的数组并填充每个元素的深拷贝。例如,在JavaScript中可以使用`JSON.parse(JSON.stringify(array))`来实现。 - **复制对象**: a. 使用构造函数:创建新对象,然后递归遍历原对象,为每个属性创建新的值,而不是引用。例如在JavaScript中,`function deepCopy(obj) { return new obj.constructor(obj); }`。 b. 使用`Object.assign()`和扩展运算符...:结合使用,创建一个新的对象,并将所有属性从源对象复制过来,但不会创建循环引用。但是这种方法不适用于原型链中的对象。 c. 库方法:有些编程语言有现成的库函数或模块可以直接完成深拷贝操作,比如Python的`copy.deepcopy()`。 3. **深度克隆(Deep Cloning)**: 术语"深度克隆"通常用于描述深度复制的过程,特别是在讨论复杂的数据结构,如树、图或对象树时。 **相关问题**: 1. 浅拷贝深拷贝的主要区别是什么? 2. 如何在JavaScript中避免对象属性的引用冲突进行深拷贝? 3. 如果数组中的元素是对象,应该如何正确地进行深拷贝
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值