深拷贝与深拷贝

189 阅读8分钟

什么是 JS 深拷贝?

深拷贝(Deep Copy) 是指创建一个新对象,并且递归地复制原对象中的所有属性,包括原对象中嵌套的对象。深拷贝后的对象与原对象互不影响,修改新对象的属性不会影响原对象,反之亦然。

与之相对的是 浅拷贝(Shallow Copy) ,它只会复制对象的第一层属性,对于嵌套的对象,拷贝的是引用,修改新对象中嵌套对象的属性,会影响到原对象。

深拷贝的实现方式

1. 使用 JSON.parseJSON.stringify 实现

这是最常见的实现深拷贝的方式,它将对象转化为字符串,然后再解析回对象。这样可以拷贝对象的所有属性,包括嵌套对象。

function deepClone(obj) {
  return JSON.parse(JSON.stringify(obj));
}
优点:
  • 实现简单,适用于大多数常见情况。
缺点:
  • 无法拷贝特殊对象:比如 DateRegExpMapSetFunctionundefined 等对象类型会丢失其特殊性质。
  • 无法处理循环引用:如果对象中存在循环引用,JSON.stringify 会抛出错误。

2. 手动递归实现深拷贝

可以通过递归遍历对象的所有属性,对每一层的属性进行拷贝,直到拷贝的对象没有嵌套对象。

function deepClone(obj) {
  if (typeof obj !== 'object' || obj === null) {
    // 如果是基本数据类型或null,则直接返回
    return obj;
  }
  
  let clone;
  
  if (Array.isArray(obj)) {
    // 如果是数组,递归拷贝每一项
    clone = [];
    for (let i = 0; i < obj.length; i++) {
      clone[i] = deepClone(obj[i]);
    }
  } else {
    // 如果是对象,递归拷贝每个属性
    clone = {};
    for (let key in obj) {
      if (obj.hasOwnProperty(key)) {
        clone[key] = deepClone(obj[key]);
      }
    }
  }

  return clone;
}
优点:
  • 支持多种数据类型:可以处理 DateRegExpMapSet 等特殊对象。
  • 可以处理循环引用:需要通过额外的代码处理循环引用。
缺点:
  • 实现稍复杂:需要递归遍历对象的每一层。

3. 使用 structuredClone(现代浏览器)

structuredClone 是浏览器原生提供的一个方法,可以实现对象的深拷贝。它能够复制各种类型的对象,包括 DateMapSetArrayBuffer 等。

javascript
复制代码
const clonedObj = structuredClone(originalObj);
优点:
  • 原生方法,性能较好。
  • 支持多种复杂数据类型(DateMapSetArrayBuffer 等)。
缺点:
  • 目前只在现代浏览器(如 Chrome 92+)中支持,不适用于旧版浏览器。
  • 不能处理函数:与 JSON.parseJSON.stringify 相似,不能处理 Functionundefined 等。

4. 使用第三方库(如 Lodash)

Lodash 提供了 cloneDeep 方法来实现深拷贝,它支持大多数复杂数据类型,性能也较好。

javascript
复制代码
import cloneDeep from 'lodash/cloneDeep';

const clonedObj = cloneDeep(originalObj);
优点:
  • 处理复杂数据类型和循环引用等问题,功能强大。
  • 性能优化。
缺点:
  • 需要引入第三方库,增加了项目的依赖和体积。

深拷贝的注意事项

  1. 特殊对象:某些对象如 DateRegExpMapSetFunctionundefined 等需要特殊处理。
  2. 循环引用:如果对象有循环引用,递归拷贝可能会导致栈溢出。需要检测并处理循环引用。
  3. 性能问题:深拷贝操作可能会对性能产生较大的影响,尤其是在对象非常大的时候。

举例:数组浅拷贝

对于数组的浅拷贝,同样的原理适用:创建一个新数组,并将原数组的第一层元素复制到新数组。如果数组中有引用类型的元素(比如对象或另一个数组),则拷贝的只是引用,而不是对象的副本。

浅拷贝的实现方法

1. 使用 slice() 方法

slice() 方法返回一个数组的浅拷贝,可以通过指定不传递参数来复制整个数组。

javascript
复制代码
const originalArray = [1, 2, 3, { name: 'Alice' }];

// 使用 slice() 进行浅拷贝
const shallowCopy = originalArray.slice();

// 修改浅拷贝的元素
shallowCopy[0] = 100;
shallowCopy[3].name = 'Bob';

console.log('originalArray:', originalArray); // [1, 2, 3, { name: 'Bob' }]
console.log('shallowCopy:', shallowCopy);      // [100, 2, 3, { name: 'Bob' }]

2. 使用 concat() 方法

concat() 方法用于合并两个或多个数组,它也能用于浅拷贝单个数组。

javascript
复制代码
const originalArray = [1, 2, 3, { name: 'Alice' }];

// 使用 concat() 进行浅拷贝
const shallowCopy = originalArray.concat();

// 修改浅拷贝的元素
shallowCopy[0] = 100;
shallowCopy[3].name = 'Bob';

console.log('originalArray:', originalArray); // [1, 2, 3, { name: 'Bob' }]
console.log('shallowCopy:', shallowCopy);      // [100, 2, 3, { name: 'Bob' }]

3. 使用扩展运算符 (...)

扩展运算符 ... 是一种简洁的数组浅拷贝方法,它可以创建原数组的浅拷贝。

javascript
复制代码
const originalArray = [1, 2, 3, { name: 'Alice' }];

// 使用扩展运算符进行浅拷贝
const shallowCopy = [...originalArray];

// 修改浅拷贝的元素
shallowCopy[0] = 100;
shallowCopy[3].name = 'Bob';

console.log('originalArray:', originalArray); // [1, 2, 3, { name: 'Bob' }]
console.log('shallowCopy:', shallowCopy);      // [100, 2, 3, { name: 'Bob' }]

4. 使用 Array.from() 方法

Array.from() 方法也可以用于浅拷贝一个数组。

javascript
复制代码
const originalArray = [1, 2, 3, { name: 'Alice' }];

// 使用 Array.from() 进行浅拷贝
const shallowCopy = Array.from(originalArray);

// 修改浅拷贝的元素
shallowCopy[0] = 100;
shallowCopy[3].name = 'Bob';

console.log('originalArray:', originalArray); // [1, 2, 3, { name: 'Bob' }]
console.log('shallowCopy:', shallowCopy);      // [100, 2, 3, { name: 'Bob' }]

浅拷贝中的引用类型问题

浅拷贝的关键在于它只复制数组的第一层内容,对于嵌套的对象(引用类型),它们会被复制为引用。因此,修改新数组中引用类型的元素,会影响到原数组中的对应元素。

javascript
复制代码
const originalArray = [{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }];

// 使用扩展运算符进行浅拷贝
const shallowCopy = [...originalArray];

// 修改浅拷贝中的对象属性
shallowCopy[0].name = 'Charlie';

console.log('originalArray:', originalArray); // [{ id: 1, name: 'Charlie' }, { id: 2, name: 'Bob' }]
console.log('shallowCopy:', shallowCopy);      // [{ id: 1, name: 'Charlie' }, { id: 2, name: 'Bob' }]

解释:

  • originalArrayshallowCopy 中的第一个元素指向同一个对象,修改其中一个数组中的对象属性,会影响另一个数组中的对象。
  • 这种行为只会发生在数组中的元素是引用类型时(如对象或数组)。

总结

  • 浅拷贝 仅复制数组的第一层,对于嵌套的对象,只复制它们的引用。
  • 可以使用 slice()concat()、扩展运算符(...)和 Array.from() 方法来实现数组的浅拷贝。
  • 引用类型 的元素在浅拷贝后,修改新数组中的对象会影响原数组中的对象。

举例:数组深拷贝

实现数组深拷贝的目的是要复制数组及其嵌套的所有对象和数组,而不仅仅是数组的第一层元素。对于深拷贝,所有嵌套的对象和数组都应该被完全复制,而不是共享引用。

1.使用 JSON.parseJSON.stringify

这种方法利用了 JSON.stringify 将对象序列化为 JSON 字符串,然后使用 JSON.parse 解析该字符串,生成一个新的对象。它可以处理大多数情况下的深拷贝,但对于含有 undefined函数RegExpSymbolDate 等特殊对象类型时,会失效。

示例代码:

const originalArray = [
  { id: 1, name: 'Alice', details: { age: 30, city: 'New York' } },
  { id: 2, name: 'Bob', details: { age: 25, city: 'Los Angeles' } }
];

// 使用 JSON.parse 和 JSON.stringify 实现深拷贝
const deepCopy = JSON.parse(JSON.stringify(originalArray));

// 修改深拷贝中的嵌套对象
deepCopy[0].details.age = 35;

// 输出结果
console.log('originalArray:', originalArray);
console.log('deepCopy:', deepCopy);

输出:

javascript
复制代码
originalArray: [
  { id: 1, name: 'Alice', details: { age: 30, city: 'New York' } },
  { id: 2, name: 'Bob', details: { age: 25, city: 'Los Angeles' } }
]
deepCopy: [
  { id: 1, name: 'Alice', details: { age: 35, city: 'New York' } },
  { id: 2, name: 'Bob', details: { age: 25, city: 'Los Angeles' } }
]

优点:

  • 简单易用,适用于大多数情况下的深拷贝。
  • 可以有效地拷贝对象的嵌套层级。

缺点:

  • 无法正确处理 undefined函数RegExpDateSymbol 等特殊类型数据。
  • 不适用于含有循环引用的对象。

2.递归实现深拷贝

递归方法可以手动实现深拷贝,通过检查对象的每一层,确保每一个对象都被独立拷贝。它不仅可以复制简单对象,还能处理包含复杂结构和引用类型的对象。

示例代码:

function deepClone(arr) {
  // 如果不是数组或对象,直接返回
  if (arr === null || typeof arr !== 'object') {
    return arr;
  }

  // 如果是数组,递归复制每个元素
  if (Array.isArray(arr)) {
    return arr.map(item => deepClone(item));
  }

  // 如果是对象,递归复制每个属性
  const newObj = {};
  for (let key in arr) {
    if (arr.hasOwnProperty(key)) {
      newObj[key] = deepClone(arr[key]);
    }
  }
  return newObj;
}

const originalArray = [
  { id: 1, name: 'Alice', details: { age: 30, city: 'New York' } },
  { id: 2, name: 'Bob', details: { age: 25, city: 'Los Angeles' } }
];

// 使用深拷贝函数
const deepCopy = deepClone(originalArray);

// 修改深拷贝中的嵌套对象
deepCopy[0].details.age = 35;

// 输出结果
console.log('originalArray:', originalArray);
console.log('deepCopy:', deepCopy);

输出:

originalArray: [
  { id: 1, name: 'Alice', details: { age: 30, city: 'New York' } },
  { id: 2, name: 'Bob', details: { age: 25, city: 'Los Angeles' } }
]
deepCopy: [
  { id: 1, name: 'Alice', details: { age: 35, city: 'New York' } },
  { id: 2, name: 'Bob', details: { age: 25, city: 'Los Angeles' } }
]

解释:

  • deepClone 函数会递归地检查对象的每一层,对于数组和对象分别使用不同的处理方式。
  • 对于数组,使用 map() 方法对每一个元素递归调用 deepClone
  • 对于对象,直接递归每个属性。

优点:

  • 适用于复杂数据结构,并且能够正确处理函数、Date 等对象类型。
  • 对于包含循环引用的对象,递归方法能适当调整处理。

缺点:

  • 代码稍复杂。
  • 如果对象中存在循环引用,递归方法会导致栈溢出,除非进行循环检测处理。

  1. 使用第三方库(例如 Lodash 的 cloneDeep

    如果不想手动实现深拷贝,可以使用第三方库,例如 Lodash,它提供了一个 cloneDeep 方法,可以高效且安全地进行深拷贝。

示例代码:使用 Lodash 的 cloneDeep

javascript
复制代码
import cloneDeep from 'lodash/cloneDeep';

const originalArray = [
  { id: 1, name: 'Alice', details: { age: 30, city: 'New York' } },
  { id: 2, name: 'Bob', details: { age: 25, city: 'Los Angeles' } }
];

// 使用 Lodash 的 cloneDeep 进行深拷贝
const deepCopy = cloneDeep(originalArray);

// 修改深拷贝中的嵌套对象
deepCopy[0].details.age = 35;

// 输出结果
console.log('originalArray:', originalArray);
console.log('deepCopy:', deepCopy);

输出:

javascript
复制代码
originalArray: [
  { id: 1, name: 'Alice', details: { age: 30, city: 'New York' } },
  { id: 2, name: 'Bob', details: { age: 25, city: 'Los Angeles' } }
]
deepCopy: [
  { id: 1, name: 'Alice', details: { age: 35, city: 'New York' } },
  { id: 2, name: 'Bob', details: { age: 25, city: 'Los Angeles' } }
]

优点:

  • 库已经优化,处理各种边界情况,包括特殊类型(例如 DateRegExpMapSet 等)。
  • 处理循环引用的能力。

缺点:

  • 需要引入外部库,增加项目依赖。