什么是 JS 深拷贝?
深拷贝(Deep Copy) 是指创建一个新对象,并且递归地复制原对象中的所有属性,包括原对象中嵌套的对象。深拷贝后的对象与原对象互不影响,修改新对象的属性不会影响原对象,反之亦然。
与之相对的是 浅拷贝(Shallow Copy) ,它只会复制对象的第一层属性,对于嵌套的对象,拷贝的是引用,修改新对象中嵌套对象的属性,会影响到原对象。
深拷贝的实现方式
1. 使用 JSON.parse
和 JSON.stringify
实现
这是最常见的实现深拷贝的方式,它将对象转化为字符串,然后再解析回对象。这样可以拷贝对象的所有属性,包括嵌套对象。
function deepClone(obj) {
return JSON.parse(JSON.stringify(obj));
}
优点:
- 实现简单,适用于大多数常见情况。
缺点:
- 无法拷贝特殊对象:比如
Date
、RegExp
、Map
、Set
、Function
、undefined
等对象类型会丢失其特殊性质。 - 无法处理循环引用:如果对象中存在循环引用,
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;
}
优点:
- 支持多种数据类型:可以处理
Date
、RegExp
、Map
、Set
等特殊对象。 - 可以处理循环引用:需要通过额外的代码处理循环引用。
缺点:
- 实现稍复杂:需要递归遍历对象的每一层。
3. 使用 structuredClone
(现代浏览器)
structuredClone
是浏览器原生提供的一个方法,可以实现对象的深拷贝。它能够复制各种类型的对象,包括 Date
、Map
、Set
、ArrayBuffer
等。
javascript
复制代码
const clonedObj = structuredClone(originalObj);
优点:
- 原生方法,性能较好。
- 支持多种复杂数据类型(
Date
、Map
、Set
、ArrayBuffer
等)。
缺点:
- 目前只在现代浏览器(如 Chrome 92+)中支持,不适用于旧版浏览器。
- 不能处理函数:与
JSON.parse
和JSON.stringify
相似,不能处理Function
、undefined
等。
4. 使用第三方库(如 Lodash)
Lodash 提供了 cloneDeep
方法来实现深拷贝,它支持大多数复杂数据类型,性能也较好。
javascript
复制代码
import cloneDeep from 'lodash/cloneDeep';
const clonedObj = cloneDeep(originalObj);
优点:
- 处理复杂数据类型和循环引用等问题,功能强大。
- 性能优化。
缺点:
- 需要引入第三方库,增加了项目的依赖和体积。
深拷贝的注意事项
- 特殊对象:某些对象如
Date
、RegExp
、Map
、Set
、Function
、undefined
等需要特殊处理。 - 循环引用:如果对象有循环引用,递归拷贝可能会导致栈溢出。需要检测并处理循环引用。
- 性能问题:深拷贝操作可能会对性能产生较大的影响,尤其是在对象非常大的时候。
举例:数组浅拷贝
对于数组的浅拷贝,同样的原理适用:创建一个新数组,并将原数组的第一层元素复制到新数组。如果数组中有引用类型的元素(比如对象或另一个数组),则拷贝的只是引用,而不是对象的副本。
浅拷贝的实现方法
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' }]
解释:
originalArray
和shallowCopy
中的第一个元素指向同一个对象,修改其中一个数组中的对象属性,会影响另一个数组中的对象。- 这种行为只会发生在数组中的元素是引用类型时(如对象或数组)。
总结
- 浅拷贝 仅复制数组的第一层,对于嵌套的对象,只复制它们的引用。
- 可以使用
slice()
、concat()
、扩展运算符(...
)和Array.from()
方法来实现数组的浅拷贝。 - 引用类型 的元素在浅拷贝后,修改新数组中的对象会影响原数组中的对象。
举例:数组深拷贝
实现数组深拷贝的目的是要复制数组及其嵌套的所有对象和数组,而不仅仅是数组的第一层元素。对于深拷贝,所有嵌套的对象和数组都应该被完全复制,而不是共享引用。
1.使用 JSON.parse
和 JSON.stringify
这种方法利用了 JSON.stringify
将对象序列化为 JSON 字符串,然后使用 JSON.parse
解析该字符串,生成一个新的对象。它可以处理大多数情况下的深拷贝,但对于含有 undefined
、函数
、RegExp
、Symbol
或 Date
等特殊对象类型时,会失效。
示例代码:
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
、函数
、RegExp
、Date
、Symbol
等特殊类型数据。 - 不适用于含有循环引用的对象。
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
等对象类型。 - 对于包含循环引用的对象,递归方法能适当调整处理。
缺点:
- 代码稍复杂。
- 如果对象中存在循环引用,递归方法会导致栈溢出,除非进行循环检测处理。
-
使用第三方库(例如 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' } }
]
优点:
- 库已经优化,处理各种边界情况,包括特殊类型(例如
Date
、RegExp
、Map
、Set
等)。 - 处理循环引用的能力。
缺点:
- 需要引入外部库,增加项目依赖。