0%

浅拷贝和深拷贝

在 javascript 中,拷贝对象或数组是常见的操作。然而,由于引用类型的存在,拷贝分为浅拷贝和深拷贝两种方式。理解它们的区别对于避免意外的数据共享和修改至关重要。

基本概念

在 javascript 中,数据类型分为基本类型(如 string、number、boolean、null、undefined、symbol、bigint)和引用类型(如 Object、Array、Function、Date、RegExp 等)。

  1. 基本类型:存储的是实际值,拷贝时直接复制值,互不影响。
  2. 引用类型:存储的是内存地址,拷贝时复制的是地址引用,多个变量可能指向同一个对象。

浅拷贝

创建一个新对象,但这个新对象会复制原对象的所有属性值。如果属性是基本类型,拷贝的就是基本类型的值;如果属性是引用类型,拷贝的就是内存地址,因此新旧对象会共享同一块内存数据,修改其中一个会影响另一个。

深拷贝

创建一个全新的对象,递归地复制原对象的所有属性。新对象和原对象完全隔离,修改互不影响。

浅拷贝的实现方式

  1. 手动赋值(只适用于简单对象)
1
2
3
4
const obj1 = { a: 1, b: { c: 2 } };
const obj2 = { a: obj1.a, b: obj1.b }; // 浅拷贝
obj2.b.c = 100;
console.log(obj1.b.c); // 100 (相互影响)
  1. Object.assign()

Object.assign(target, …sources) 将源对象的可枚举属性复制到目标对象,返回目标对象。它是浅拷贝。

1
2
3
4
const obj1 = { a: 1, b: { c: 2 } };
const obj2 = Object.assign({}, obj1);
obj2.b.c = 100;
console.log(obj1.b.c); // 100
  1. 展开运算符 …
1
2
3
4
const obj1 = { a: 1, b: { c: 2 } };
const obj2 = { ...obj1 };
obj2.b.c = 100;
console.log(obj1.b.c); // 100
  1. 数组的浅拷贝方法

数组也是对象,许多数组方法返回的是浅拷贝。

  • slice():
1
2
3
4
const arr1 = [1, 2, { a: 3 }];
const arr2 = arr1.slice();
arr2[2].a = 100;
console.log(arr1[2].a); // 100
  • concat():
1
const arr2 = arr1.concat();
  • 展开运算符:
1
const arr2 = [...arr1];
  • Array.from():
1
const arr2 = Array.from(arr1);

深拷贝的实现方式

JSON.parse(JSON.stringify(obj))

这是最常用的简单深拷贝方法,但存在一些限制:

  • 无法拷贝函数、undefined、Symbol、BigInt。
  • 无法处理循环引用(会抛出错误)。
  • 特殊对象如 Date、RegExp、Map、Set 等会被序列化为字符串或空对象。
  • 会丢弃对象的 constructor,原型链不再保留。
1
2
3
4
5
6
7
8
9
10
11
const obj1 = {
a: 1,
b: { c: 2 },
d: new Date(),
e: undefined,
f: function() {},
g: Symbol('g')
};
const obj2 = JSON.parse(JSON.stringify(obj1));
console.log(obj2);
// { a: 1, b: { c: 2 }, d: "2024-01-25T..." } // 函数、undefined、Symbol 丢失,Date 变成了字符串

递归实现深拷贝(基础版)

手动编写递归函数,处理基本类型和对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function deepClone(obj) {
if (obj === null || typeof obj !== 'object') return obj;
if (obj instanceof Date) return new Date(obj);
if (obj instanceof RegExp) return new RegExp(obj);
// 处理数组和普通对象
const cloneObj = Array.isArray(obj) ? [] : {};
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
cloneObj[key] = deepClone(obj[key]);
}
}
return cloneObj;
}

const obj1 = { a: 1, b: { c: 2 }, d: [1, 2, { e: 3 }] };
const obj2 = deepClone(obj1);
obj2.b.c = 100;
console.log(obj1.b.c); // 2

该基础版未处理循环引用、Map、Set、Symbol 属性等,但已满足大部分场景。

处理循环引用

使用 WeakMap 记录已拷贝的对象,遇到循环引用直接返回记录。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
function deepClone(obj, hash = new WeakMap()) {
if (obj === null || typeof obj !== 'object') return obj;
if (hash.has(obj)) return hash.get(obj); // 处理循环引用

const clone = Array.isArray(obj) ? [] : {};
hash.set(obj, clone);

// 处理 Symbol 属性
const symKeys = Object.getOwnPropertySymbols(obj);
if (symKeys.length) {
symKeys.forEach(symKey => {
clone[symKey] = deepClone(obj[symKey], hash);
});
}

// 处理普通字符串属性
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
clone[key] = deepClone(obj[key], hash);
}
}
return clone;
}

// 测试循环引用
const obj = { a: 1 };
obj.self = obj;
const cloned = deepClone(obj);
console.log(cloned.self === cloned); // true

使用第三方库

lodash 的 _.cloneDeep():功能完善,处理了各种边界情况。

1
2
const _ = require('lodash');
const obj2 = _.cloneDeep(obj1);

jQuery 的 $.extend(true, {}, obj)。

结构化克隆(Structured Clone)

现代浏览器提供 structuredClone() 全局函数,用于深拷贝,支持大部分内置类型(如 Date、RegExp、Map、Set、ArrayBuffer 等),但仍不能拷贝函数、Symbol、DOM 节点等。

1
2
3
const obj1 = { a: 1, b: { c: 2 }, d: new Date() };
const obj2 = structuredClone(obj1);
console.log(obj2); // 正确深拷贝

浅拷贝与深拷贝对比示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 原始对象
const original = {
name: 'Alice',
age: 25,
address: {
city: 'New York',
zip: 10001
},
hobbies: ['reading', 'gaming']
};

// 浅拷贝
const shallowCopy = { ...original };

// 深拷贝(JSON 方法)
const deepCopy = JSON.parse(JSON.stringify(original));

// 修改浅拷贝的嵌套对象
shallowCopy.address.city = 'Los Angeles';
shallowCopy.hobbies.push('swimming');
console.log(original.address.city); // 'Los Angeles' (原对象被改变)
console.log(original.hobbies); // ['reading', 'gaming', 'swimming'] (原对象被改变)

// 修改深拷贝的嵌套对象
deepCopy.address.city = 'Chicago';
deepCopy.hobbies.push('running');
console.log(original.address.city); // 仍为 'Los Angeles' (不受影响)
console.log(original.hobbies); // 仍为 ['reading', 'gaming', 'swimming'] (不受影响)

特殊场景与注意事项

  1. 函数和 undefined 属性:JSON.stringify 会丢失函数和 undefined,递归实现可以保留函数(拷贝函数引用),但通常函数是无状态的,浅拷贝即可。
  2. Symbol 作为属性名:JSON.stringify 会忽略 Symbol 属性,递归拷贝时可借助 Object.getOwnPropertySymbols 处理。
  3. Date、RegExp、Map、Set 等:JSON.stringify 会将其转换为字符串或普通对象,导致信息丢失。递归拷贝时应针对这些类型特殊处理。
  4. 原型链:浅拷贝和大多数深拷贝方法都不会复制原型链上的属性,只拷贝对象自身的可枚举属性。如果需要保留原型,可使用 Object.create(Object.getPrototypeOf(obj)) 并结合 Reflect.ownKeys 等方式。
  5. 性能:深拷贝递归遍历对象,可能影响性能,对于大型对象应谨慎使用,或考虑 immutable 数据结构。

总结

特性 浅拷贝 深拷贝
是否创建新对象
复制基本类型值 复制值 复制值
复制引用类型 复制引用(共享内存) 递归复制新对象
修改嵌套属性影响原对象 不会
实现复杂度 简单(内置方法) 较复杂(需递归)
常见实现 Object.assign、…、Array.slice() JSON.parse(JSON.stringify())、递归、structuredClone、lodash

选择浅拷贝还是深拷贝取决于需求:

  1. 如果对象只有一层属性(所有属性都是基本类型),浅拷贝即可。
  2. 如果对象包含嵌套结构,且希望完全隔离,必须使用深拷贝。
  3. 注意深拷贝的性能和边界情况,必要时选择成熟库或原生 structuredClone。