shallow copy reference, deep copy content heap
在 JavaScript 中,浅拷贝(Shallow Copy) 和 深拷贝(Deep Copy) 的核心区别在于它们如何处理引用类型的数据(如对象和数组)在 堆(Heap) 中的存储。以下是详细解析:
1. JavaScript 内存模型:堆与栈
-
栈(Stack):
- Number, String, Boolean, null, undefined, Symbol
- 用于存储基本数据类型(如
number
、string
、boolean
等)和引用类型的地址。 - 基本数据类型直接存储值。
- 引用类型存储的是指向堆中实际数据的引用地址。
-
堆(Heap):
- Object, Array, Function
- 用于存储引用类型(如对象和数组)的实际内容。
- 引用类型的变量在栈中存储的是指向堆中数据的地址。
2. 浅拷贝与深拷贝的堆内存模型
浅拷贝(Shallow Copy)
- 行为:
- 浅拷贝只复制对象的第一层属性。
- 如果属性是基本数据类型,直接复制值。
- 如果属性是引用类型,复制的是引用地址,而不是实际内容。
- 堆内存结果:
- 浅拷贝后的对象与原对象共享堆中的引用类型数据。
深拷贝(Deep Copy)
- 行为:
- 深拷贝会递归复制对象的所有层级内容。
- 对于引用类型,会创建新的堆内存空间,存储独立的副本。
- 堆内存结果:
- 深拷贝后的对象与原对象完全独立,修改其中一个不会影响另一个。
浅拷贝(Shallow Copy)的内存模型
// 原始对象
const obj1 = {
a: 1,
b: {
c: 2
}
};
// 浅拷贝
const obj2 = { ...obj1 };
内存示意图
栈内存(Stack) 堆内存(Heap)
───────────── ────────────────
obj1 ─────────┐ { a: 1,
│ b: ─────┐
obj2 ─────────┘ } │
│
{ c: 2 } ◄
- 解释:
obj1
和obj2
是两个不同的对象引用(在栈中)。- 但它们的
b
属性指向堆内存中的同一个对象。 - 修改
obj1.b.c
会影响obj2.b.c
,因为它们共享同一个引用。
示例代码
const obj1 = { a: 1, b: { c: 2 } };
const obj2 = { ...obj1 };
// 修改基本类型属性
obj1.a = 10;
console.log(obj2.a); // 输出: 1(不受影响)
// 修改引用类型属性
obj1.b.c = 20;
console.log(obj2.b.c); // 输出: 20(受影响)
深拷贝(Deep Copy)的内存模型
// 原始对象
const obj1 = {
a: 1,
b: {
c: 2
}
};
// 深拷贝
const obj2 = JSON.parse(JSON.stringify(obj1));
内存示意图
栈内存(Stack) 堆内存(Heap)
───────────── ────────────────
obj1 ─────────► { a: 1,
b: ─────┐
} │
│
{ c: 2 } ◄
obj2 ─────────► { a: 1,
b: ─────┐
} │
│
{ c: 2 } ◄
- 解释:
obj1
和obj2
指向堆内存中的不同对象。- 它们的
b
属性也指向不同的对象。 - 修改
obj1.b.c
不会影响obj2.b.c
,因为它们是完全独立的。
示例代码
const obj1 = { a: 1, b: { c: 2 } };
const obj2 = JSON.parse(JSON.stringify(obj1));
// 修改基本类型属性
obj1.a = 10;
console.log(obj2.a); // 输出: 1(不受影响)
// 修改引用类型属性
obj1.b.c = 20;
console.log(obj2.b.c); // 输出: 2(不受影响)
3. 内存分配的详细比较
1. 浅拷贝
const original = {
name: "张三",
age: 18,
address: {
city: "北京",
district: "朝阳"
}
};
const shallow = { ...original };
内存分配:
栈内存 堆内存
────────── ───────────────────
original ──┐ {
│ name: "张三",
├──────────────────► age: 18,
│ address: ────┐
shallow ───┘ } │
│
{ │
city: "北京", ◄
district: "朝阳"
}
2. 深拷贝
const original = {
name: "张三",
age: 18,
address: {
city: "北京",
district: "朝阳"
}
};
const deep = JSON.parse(JSON.stringify(original));
内存分配:
栈内存 堆内存
────────── ───────────────────
original ──────────────────► {
name: "张三",
age: 18,
address: ────┐
} │
│
{ │
city: "北京" ◄
district: "朝阳"
}
deep ─────────────────────► {
name: "张三",
age: 18,
address: ────┐
} │
│
{ │
city: "北京" ◄
district: "朝阳"
}
性能影响
-
浅拷贝:
- 内存占用较少
- 复制速度快
- 适合简单对象的复制
-
深拷贝:
- 内存占用较多(需要复制所有嵌套对象)
- 复制速度较慢(需要递归处理)
- 适合需要完全独立副本的场景
总结
特性 | 浅拷贝 | 深拷贝 |
---|---|---|
内存分配 | 只在栈中创建新的引用 | 在堆中创建所有对象的副本 |
引用关系 | 共享引用类型的内存 | 完全独立的内存空间 |
内存占用 | 较少 | 较多 |
性能 | 较好 | 较差 |
适用场景 | 简单对象,临时副本 | 需要独立副本,数据隔离 |
4. 浅拷贝与深拷贝的堆内存对比
特性 | 浅拷贝 | 深拷贝 |
---|---|---|
基本数据类型 | 直接复制值 | 直接复制值 |
引用类型 | 复制引用地址(共享堆内存) | 创建新的堆内存空间,存储独立副本 |
修改影响 | 修改原对象会影响拷贝对象 | 修改原对象不会影响拷贝对象 |
性能 | 性能较高(只复制第一层) | 性能较低(递归复制所有层级) |
5. 深拷贝与浅拷贝的实现方式
浅拷贝实现
-
扩展运算符(
...
)const obj2 = { ...obj1 };
-
Object.assign
const obj2 = Object.assign({}, obj1);
-
数组的浅拷贝
const arr2 = [...arr1]; // 或 arr1.slice();
深拷贝实现
-
JSON.parse(JSON.stringify(obj)
- 简单易用,但有局限性(无法拷贝函数、
undefined
、Symbol
,无法处理循环引用)。
const obj2 = JSON.parse(JSON.stringify(obj1));
- 简单易用,但有局限性(无法拷贝函数、
-
递归拷贝
- 手动实现深拷贝,适合处理复杂对象。
function deepCopy(obj) { if (obj === null || typeof obj !== "object") { return obj; } const copy = Array.isArray(obj) ? [] : {}; for (const key in obj) { copy[key] = deepCopy(obj[key]); } return copy; } const obj2 = deepCopy(obj1);
-
使用第三方库
- Lodash 提供了
_.cloneDeep
方法。
const _ = require('lodash'); const obj2 = _.cloneDeep(obj1);
- Lodash 提供了
6. 总结:如何选择?
浅拷贝适用场景
- 对象层级较浅,且不需要修改嵌套对象时。
- 性能要求较高时。
- 示例:复制简单的配置对象。
深拷贝适用场景
- 对象层级较深,且需要修改嵌套对象时。
- 需要完全独立的副本时。
- 示例:复制复杂的嵌套数据结构。
7. 图解:堆内存中的浅拷贝与深拷贝
浅拷贝
obj1 -> { a: 1, b: { c: 2 } }
obj2 -> { a: 1, b: { c: 2 } } // 共享 b 的引用
深拷贝
obj1 -> { a: 1, b: { c: 2 } }
obj2 -> { a: 1, b: { c: 2 } } // b 是独立的副本
通过理解堆内存模型,可以更好地选择浅拷贝或深拷贝方法,满足不同的开发需求。