讲解 JavaScript 中的深拷贝和浅拷贝
一、核心概念:数据类型与内存存储
要理解深浅拷贝,首先要明白 JavaScript 的数据类型是如何存储的。
基本数据类型 (Primitive Types)
包括:
String
,Number
,Boolean
,null
,undefined
,Symbol
,BigInt
。存储方式:值本身直接存储在栈内存中。
赋值操作:将一个变量赋值给另一个变量时,会创建一个全新的副本。两个变量互不影响。
let a = 10; let b = a; // 在栈内存中创建一个新的值 10,赋值给 bb = 20; console.log(a); // 10 (a 的值没有改变) console.log(b); // 20
对于基本类型,赋值即等于深拷贝,不存在浅拷贝问题。
引用数据类型 (Reference Types)
包括:
Object
,Array
,Function
,Date
等。存储方式:变量在栈内存中存储的是一个地址(指针),这个地址指向堆内存中存储的真正的对象数据。
赋值操作:将一个对象赋值给另一个变量时,复制的是地址(指针),而不是堆内存中的数据本身。
let obj1 = { name: 'Alice', age: 25 }; let obj2 = obj1; // 只复制了地址,现在 obj1 和 obj2 指向堆内存中的同一个对象obj2.name = 'Bob'; // 通过 obj2 的地址修改了堆内存中的数据console.log(obj1.name); // 'Bob' (obj1 也变了!) console.log(obj2.name); // 'Bob'
这种只复制地址,导致多个变量共享同一份数据的拷贝,就是浅拷贝。
二、什么是浅拷贝 (Shallow Copy)?
定义:创建一个新对象,这个新对象有着原始对象属性值的一份精确拷贝。
如果属性是基本类型,拷贝的就是基本类型的值。
如果属性是引用类型,拷贝的就是内存地址(指针)。
结果:新对象和原对象中的引用类型属性会指向同一个堆内存地址。修改其中一个,另一个也会随之改变。
实现浅拷贝的常用方法:
展开运算符 (
...
)const original = { a: 1, b: { c: 2 } }; const shallowCopy = { ...original };original.a = 999; // 修改基本类型属性 console.log(shallowCopy.a); // 1 (未受影响,因为是值的拷贝)original.b.c = 888; // 修改引用类型内部的属性 console.log(shallowCopy.b.c); // 888 (也变了!因为它们共享同一个 { c: 2 } 对象)
Object.assign()
const original = { a: 1, b: { c: 2 } }; const shallowCopy = Object.assign({}, original); // 同样的效果,修改 original.b.c 会导致 shallowCopy.b.c 也变化
Array.prototype.slice()
和Array.prototype.concat()
(针对数组)const originalArray = [1, 2, { name: 'Alice' }]; const shallowCopyArray = originalArray.slice();originalArray[2].name = 'Bob'; console.log(shallowCopyArray[2].name); // 'Bob' (也变了)
三、什么是深拷贝 (Deep Copy)?
定义:创建一个新对象,并递归地拷贝原始对象中的所有属性及其嵌套的子对象。
无论属性是基本类型还是引用类型,都会在堆内存中创建一个全新的副本。
结果:新对象和原对象完全分离,互不干扰。修改任何一个对象,都不会影响另一个。
实现深拷贝的方法:
JSON.parse(JSON.stringify())
(最常用但有缺陷)const original = { a: 1, b: { c: 2 } }; const deepCopy = JSON.parse(JSON.stringify(original));original.b.c = 888; console.log(deepCopy.b.c); // 2 (未受影响,完全独立)
⚠️ 缺点:
无法拷贝函数、
undefined
、Symbol
。无法处理循环引用(对象属性间接或直接引用自身),会报错。
会丢弃对象的
constructor
,所有对象都会变成Object
。特殊对象如
Date
会变成字符串,RegExp
、Error
对象等会变成空对象{}
。
手动递归实现
function deepClone(source) {// 如果不是对象或者是null,直接返回(基础类型和null)if (typeof source !== 'object' || source === null) {return source;}// 处理数组和对象const target = Array.isArray(source) ? [] : {};for (let key in source) {if (source.hasOwnProperty(key)) {// 递归拷贝每一个属性target[key] = deepClone(source[key]);}}return target; }const obj = { a: 1, b: { c: 2 } }; const clonedObj = deepClone(obj);
这种方法更可靠,但需要自己处理边界情况(如循环引用)。
使用第三方库
这是生产环境中最可靠、最省事的方法。著名库如 Lodash 提供了经过充分测试的_.cloneDeep()
方法。import _ from 'lodash'; const original = { a: 1, b: { c: 2 } }; const deepCopy = _.cloneDeep(original);
四、总结与对比
特性 | 浅拷贝 (Shallow Copy) | 深拷贝 (Deep Copy) |
---|---|---|
核心区别 | 只复制一层, deeper 引用类型属性共享同一内存 | 递归复制所有层级,所有属性都独立 |
拷贝内容 | 基本类型值,引用类型地址 | 所有层级的值 |
修改效果 | 修改原对象引用类型属性会影响拷贝对象 | 修改原对象任何属性都不会影响拷贝对象 |
实现方法 | ... , Object.assign() , array.slice() | _.cloneDeep() , JSON 方法, 手动递归 |
性能 | 快,占用内存少 | 慢,占用内存多(需要递归创建所有对象) |
如何选择?
需要浅拷贝时:当你确信对象没有嵌套引用类型,或者你希望嵌套对象是共享的(例如,共享一个配置对象)。
需要深拷贝时:当你想创建一个完全独立、与原始对象毫无关联的副本,防止意外的副作用。这在状态管理(如 Redux)、函数式编程中非常常见。
理解深浅拷贝的关键在于彻底弄懂 JavaScript 的堆栈存储机制和引用类型的概念。希望这个讲解对你有帮助!