JavaScript 中的浅拷贝与深拷贝全面解析

JavaScript 中的浅拷贝与深拷贝全面解析

在实际开发中,“修改一个对象,另一个对象也跟着变了”是非常常见的坑,其根源就在于:引用类型的赋值是“引用传递”。理解浅拷贝与深拷贝,是解决这类问题的关键。

本文将从以下几个方面系统讲清楚:

  • 基本类型 vs 引用类型在内存中的差异
  • 什么是浅拷贝?有哪些常见实现方式?
  • 什么是深拷贝?JSON.parse(JSON.stringify()) 有哪些坑?
  • 如何手写一个相对实用的深拷贝函数?
  • 在工程中如何选择合适的拷贝方式?

一、值类型与引用类型的赋值差异

1. 基本类型(按值传递)

1
2
3
4
let a = 1;
let b = a;
b = 2;
console.log(a, b); // 1, 2

b 的修改不会影响 a

2. 引用类型(按引用传递)

1
2
3
4
const obj1 = { x: 1 };
const obj2 = obj1;
obj2.x = 2;
console.log(obj1.x); // 2

这里 obj1obj2 指向的是同一个对象,所以任何一方的修改都会体现在另一方上。

为了解决这种“互相影响”的问题,就引出了浅拷贝与深拷贝。


二、浅拷贝:只拷贝第一层

定义:

浅拷贝会创建一个新对象,将原对象的第一层属性复制过去。
若属性值是引用类型,则拷贝的是“引用地址”,两者仍指向同一个子对象。

1. 常见浅拷贝方式

(1)Object.assign

1
2
3
4
5
6
7
8
const obj = { a: 1, b: { c: 2 } };
const copy = Object.assign({}, obj);

copy.a = 10;
copy.b.c = 20;

console.log(obj.a); // 1
console.log(obj.b.c); // 20(被影响)

(2)展开运算符 …

1
2
const obj = { a: 1, b: { c: 2 } };
const copy = { ...obj };

效果与 Object.assign({}, obj) 相同:都是浅拷贝。

(3)数组的 slice / concat

1
2
3
const arr = [1, { x: 2 }];
const copy1 = arr.slice();
const copy2 = arr.concat();

对数组元素本身的修改互不影响,但对元素内部对象的修改会互相影响。

2. 适用场景

  • 对象/数组只有一层结构;
  • 或你只关心浅层属性,深层属性本身会被整体替换。

例如在 Redux 中,常常通过:

1
return { ...state, count: state.count + 1 };

来创建新状态对象;若某个字段是大对象,通常会整体替换,以避免深层共享引用带来的 bug。


三、深拷贝:复制整个对象图

定义:

深拷贝会递归复制对象的所有层级,生成一个与原对象“结构相同但引用完全独立”的新对象。

修改新对象的任何嵌套属性,都不会影响到原对象。

1. 最常见的“快捷方式”:JSON.parse(JSON.stringify())

1
2
3
4
5
const obj = { a: 1, b: { c: 2 } };
const copy = JSON.parse(JSON.stringify(obj));

copy.b.c = 20;
console.log(obj.b.c); // 2

看起来很好用,但存在诸多限制:

  • 无法处理:
    • undefined
    • Symbol
    • Function
    • Date(会变成字符串)
    • RegExp(会变成空对象)
    • Map / Set / WeakMap / WeakSet
    • 循环引用(会直接报错)

示例:

1
2
3
4
5
6
7
8
9
10
const obj = {
a: 1,
b: undefined,
c: () => {},
d: Symbol("d"),
e: new Date(),
};

console.log(JSON.parse(JSON.stringify(obj)));
// { a: 1, e: "2024-03-27T10:00:00.000Z" } // b/c/d 丢失,e 变字符串

因此,JSON 方案只适用于:

  • 数据结构简单、只包含普通对象/数组/数字/字符串/布尔值/null 的情况;
  • 并且可以接受对 Date 等类型的“降级”。

四、手写一个相对实用的深拷贝函数

下面实现一个简单但实用性较高的 deepClone

  • 支持:ObjectArrayDateRegExpMapSet
  • 能处理循环引用
  • 不复制原型链上的属性(只复制自有属性)
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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
function isObject(value) {
return value !== null && typeof value === "object";
}

function getType(value) {
return Object.prototype.toString.call(value).slice(8, -1);
}

function deepClone(source, weakMap = new WeakMap()) {
if (!isObject(source)) return source;

// 处理循环引用
if (weakMap.has(source)) {
return weakMap.get(source);
}

const type = getType(source);
let target;

switch (type) {
case "Date":
target = new Date(source.getTime());
break;
case "RegExp":
target = new RegExp(source.source, source.flags);
break;
case "Map":
target = new Map();
weakMap.set(source, target);
source.forEach((value, key) => {
target.set(key, deepClone(value, weakMap));
});
return target;
case "Set":
target = new Set();
weakMap.set(source, target);
source.forEach((value) => {
target.add(deepClone(value, weakMap));
});
return target;
case "Array":
target = [];
break;
default:
target = {};
}

weakMap.set(source, target);

// 只复制自有属性,不遍历原型链
for (const key of Object.keys(source)) {
target[key] = deepClone(source[key], weakMap);
}

return target;
}

1. 处理循环引用

1
2
3
4
5
const obj = { a: 1 };
obj.self = obj;

const copy = deepClone(obj);
console.log(copy.self === copy); // true

如果不使用 WeakMap 记录已克隆过的对象,这种结构会导致无限递归。


五、工程实践中的选择建议

1. 优先考虑“不可变数据结构”的设计

在很多场景下,与其到处做深拷贝,不如在设计阶段:

  • 让数据结构尽量“扁平化”
  • 尽量避免深层嵌套
  • 通过 ID 引用关联数据,而不是嵌套层层对象

这样可以:

  • 降低拷贝成本
  • 提高 diff/比较效率(如 Redux/Vue/React 的更新逻辑)

2. 选择合适拷贝方式

场景 推荐方式
浅层对象、只关心第一层 Object.assign 或 展开运算符
简单数据(只含 JSON 兼容类型) JSON.parse(JSON.stringify())
复杂嵌套结构、包含 Date/Map/Set 使用手写 deepClone 或专业库(如 lodash 的 cloneDeep
性能敏感、数据更新频繁 考虑不拷贝大对象,而是通过不可变数据模式、结构共享等优化

3. 注意性能与“过度复制”

深拷贝是有成本的:

  • 递归遍历整个对象图
  • 创建大量新对象与数组

在大型数据结构上频繁深拷贝,可能带来明显的性能问题,应尽量避免:

  • 在频繁更新的热路径上做深拷贝
  • 在动画帧、高频事件中深拷贝大对象

六、总结

浅拷贝与深拷贝的本质区别在于:

  • 浅拷贝:只复制一层,对象内部的引用类型属性仍指向同一个对象
  • 深拷贝:递归复制所有层级,生成一个结构完全相同但引用完全独立的新对象

日常开发中:

  • 学会区分哪种场景只需要浅拷贝,哪种场景必须深拷贝
  • 了解 JSON.parse(JSON.stringify()) 的适用边界与坑
  • 在需要更强能力时,使用手写 deepClone 或成熟的第三方库

有了这些基础,再结合不可变数据模式与状态管理库(Redux/MobX/Pinia 等),你就能更加自信地处理复杂状态与数据结构的变更,而不必担心“改了这边,那边也跟着变”的诡异问题。


JavaScript 中的浅拷贝与深拷贝全面解析
https://sunjc.vip/2024/03/27/浅拷贝与深拷贝全面解析/
作者
Sunjc
发布于
2024年3月27日
许可协议