深拷贝和浅拷贝的那些事
拷贝分为深拷贝(deep copy)和浅拷贝(shallow copy),这两种都为编程中常见的概念,特别是在处理数据结构和对象时很重要,本文将阐述这两者之间的区别,让你对这种初步认识起来像是复制粘贴似的玩意有更深的认知。
深拷贝与浅拷贝
首先需要知道,拷贝(copy) 这一概念通常只针对引用类型,是指将一个对象的值复制(或引用)到另一个对象中的操作,按修改原对象的值是否会影响到新对象分为浅拷贝和深拷贝。
浅拷贝:
基于原对象,拷贝得到一个新的对象,原对象中内容的修改会影响新对象。
深拷贝:
基于原对象,拷贝得到一个新的对象,原对象中内容的修改不会影响新对象。
那么,造成深拷贝与浅拷贝区别的原因是什么呢?答案就在以下两张图中。


通俗来讲,浅拷贝只复制对象本身,而不复制对象内部的引用对象,这意味着新对象与原对象共享同一组内部对象。而深拷贝则会递归复制所有引用对象,确保所有对象都是独立的。
那么,实现深浅拷贝的方法又有哪些呢?
浅拷贝的实现方法:
无论是实现方法还是应用场景,浅拷贝都占了大多数。这是因为浅拷贝的实现相对简单,对于许多应用场景已经足够。浅拷贝的主要优势在于效率和内存使用上,因为它只复制对象本身,而不会递归复制整个对象图。
1.Object.create(obj)
首先是大家喜闻乐见的Object.create(obj)
。可以看���,修改obj
中的值obj2
的值也随之改变,因此这种方法为浅拷贝。
let obj={
a:1
}
let obj2=Object.create(obj);
obj.a=2;
console.log(obj2.a); // 输出2
2.Object.assign({}, obj)
接下来是Object.assign({}, obj)
。这种方法实现拷贝的原理,是将一个或多个原对象(obj
)的所有可枚举属性复制到目标对象({}
)中,并返回目标对象。需要注意的是:这种方法可以改变原对象的引用类型而改变新对象,而原始类型的改变不会影响新对象,正因为如此,它还是被归为浅拷贝。
// 原始对象
const obj = {
name: '小明',
like: {
n: 'running'
}
};
// 浅拷贝得到新对象
const shallowCopy = Object.assign({}, obj);
// 修改原对象的值
obj.name = "小红"; // 原对象的原始类型修改不会改变新对象中的值
obj.like.n = 'swimming' // 原对象的引用类型修改才会改变新对象中的值
// 输出结果
console.log('Shallow Copy:', shallowCopy); // 输出Shallow Copy: { name: '小明', like: { n: 'swimming' } }
3.[].concat(arr) :
接下来要介绍的是数组的拷贝方法[].concat(arr)
。它的原理是将arr
中的元素合并到[]
中,并返回一个新数组。同样的,改变原对象的引用类型会改变新对象,而原始类型的改变不会影响新对象。
// 原始数组
const arr = [1, 2, 3,{a:"小明"}];
// 浅拷贝得到新数组
const shallowCopy = [].concat(arr);
// 修改原数组的值
arr[1] = 20; // 不会改变新对象
arr[3].a = '小红'; // 会改变新对象
// 输出结果
console.log('Shallow Copy Array:', shallowCopy); // 输出Shallow Copy Array: [ 1, 2, 3, { a: '小红' } ]
4.[...arr]:
数组解构赋值 [...arr]
,这种方法就是将数组中的元素全部剖析出来。提取元素,除去表壳,通过数组解构赋值创建一个新数组,复制原数组的元素到新数组中。它的执行结果同上种方法极为相似。
// 原始数组
const arr = [1, 2, 3,{a:"小明"}];
// 浅拷贝得到数组
const shallowCopy = [...arr];
// 修改原数组的值
arr[1] = 20; // 不会改变新对象
arr[3].a = '小红'; // 会改变新对象
// 输出结果
console.log('Shallow Copy Array:', shallowCopy); // 输出Shallow Copy Array: [ 1, 2, 3, { a: '小红' } ]
5.arr.slice() :
slice()
方法原本是从现有数组中提取出指定范围的元素(左开右闭),然后返回这些元素组成的新数组,而不会修改原始数组。
使用数组的 slice()
方法创建一个新数组,包含原数组的所有元素,也是一种常见的拷贝方法。
// 原始数组
const arr = [1, 2, 3,{a:"小明"}];
// 浅拷贝得到数组
const shallowCopy = arr.slice(0);
// 修改原数组的值
arr[1] = 20; // 不会改变新对象
arr[3].a = '小红'; // 会改变新对象
// 输出结果
console.log('Shallow Copy Array:', shallowCopy); // 输出Shallow Copy Array: [ 1, 2, 3, { a: '小红' } ]
6.arr.reverse().reverse():
最后一种方法,arr.reverse().reverse()
,它实际上是对原数组进行了两次反转操作,结果是将数组的顺序恢复到最初的顺序。
同样的,它也能由此实现浅拷贝。
const arr = [1, 2, 3, 4];
arr.reverse(); // 第一次反转,原数组变为 [4, 3, 2, 1]
arr.reverse(); // 第二次反转,原数组再次变为 [1, 2, 3, 4]
手写一个浅拷贝
好了,这下浅拷贝原理思路一下子清晰了,下面就让我们手写一个浅拷贝。
let obj = {
name: '小明',
like: {
a: 'food'
}
}
function shallow(obj){
let newObj = {}
for(let key in obj){
if(obj.hasOwnProperty(key)) {
newObj[key] = obj[key]
}
}
return newObj
}
let obj2 = shallow(obj)
console.log(obj2); //输出{ name: '小明', like: { a: 'food' } }
obj.like.a = 'beer' //修改原对象的值
console.log(obj2); //输出{ name: '小明', like: { a: 'beer' } }
实现原理:
-
借助
for in
遍历原对象,将原对象属性增加在新对象中 -
因为
for in
会遍历到对象隐式具有的属性,通常要使用obj.hasOwnProperty(key)
来判断要拷贝的属性是不是对象显示具有的
其中的shallow
函数遍历了原始对象obj
的所有可枚举属性,并将它们复制到一个新的空对象newObj
中。需要注意的是,这里只复制了对象的第一层属性,如果属性的值是对象,那么复制的是对象的引用,这也是这个函数能实现浅拷贝的原因。
深拷贝的实现方法:
1.JSON.parse(JSON.stringify(obj)):
JSON.parse(JSON.stringify(obj))
会产生一个与原对象具有相同数据的新对象实例,且两者之间没有引用关系,但这种方法还是存在以下限制:
- 不能识别
BigInt
类型。 - 不能拷贝
undefined
、Symbol
、function
。 - 不能处理循环引用。
let obj = {
name: '小明',
age: 18,
like: {
n:'running'
},
a: true,
b: undefined,
c:null,
d: Symbol(1),
f: function() {}
}
let obj2 = JSON.parse(JSON.stringify(obj));
obj.like.n = 'swimming';
console.log(obj);
console.log(obj2);
输出的结果为:
可以看到这种方法没能识别到undefined
、Symbol
、function
三种类型。而且如果我们添加上e:123n
这个大整形的话则会报错。
2.structuredClon():
第二种也是JS官方推出的一种深拷贝方法structuredClon()
可谓是千呼万唤始出来啊。
// 原始对象
let obj={
name: '小明',
like: {
n: 'running'
}
}
// 深拷贝得到对象
const shallowCopy = structuredClone(obj);
// 修改原对象的值
obj.name = "小红";
obj.like.n = 'swimming'
// 输出结果
console.log('Shallow Copy:', shallowCopy); //输出Shallow Copy: { name: '小明', like: { n: 'running' } }
无论是修改原对象中的原始类型还是引用类型,新对象愣是纹丝不动,深拷贝的成果堪称完美。
手写一个深拷贝
又到了我们的惯例手写环节,这不,就让我们手写实现一个深拷贝。
let obj = {
name: '小明',
like: {
a: 'food'
}
}
function deep(obj) {
let newObj = {}
for (let key in obj) {
if (obj.hasOwnProperty(key)) {//规避隐式原型属性
//判断是不是对象
if (obj[key] instanceof Object) {
//typeof obj[key]==='object' && obj[key]!==bull
newObj[key] = deep(obj[key])
}
else {
newObj[key] = obj[key]
}
}
}
return newObj
}
const obj2 = deep(obj)
console.log(obj2); //输出 { name: '小明', like: { a: 'food' } }
obj.name = '小红' //修改原对象的值
obj.like.a = 'beer'
console.log(obj2); //输出 { name: '小明', like: { a: 'food' } }
实现原理
- 借助
for in
遍历原对象,将原对象属性增加在新对象中 - 因为
for in
会遍历到对象隐式具有的属性,通常要使用obj.hasOwnProperty(key)
来判断要拷贝的属性是不是对象显示具有的 - 如果遍历到的属性值是原始值类型,直接往新对象中赋值,如果是引用类型,递归创建新的子对象
最后:
总结一下就是:
浅拷贝方法:
Object.create(x)
Object.assign({}, a)
[].concat(arr)
数组解构 let newArr = [...arr]
arr.slice()
arr.toReversed().reverse()
深拷贝方法:
JSON.parse(JSON.stringify(obj))
- 不能识别
BigInt
类型 - 不能拷贝
undefined
Symbol
function
- 不能处理循环引用
structuredClone();
希望大家能通过本文对深浅拷贝能有进一步认知。

转载自:https://juejin.cn/post/7390593301508866060