JS 深入了解深拷贝和浅拷贝的区别,实现一个简单的深拷贝(基础篇)
壹 ❀ 引
深浅拷贝属于js中一个比较基础但重要的概念,日常开发中很多地方都会需要使用到深拷贝,在操作数据时,我们都应该潜意识的思考是否会对原数据造成影响。而且在面试时,我也遇到过如何实现一个深拷贝,需要注意哪些边界情况的问题,对于初学者而言,了解这个概念是非常有必要的。
本文是我在17年刚从事前端不久时所写的一篇文章,我觉得作为基础入门去了解深拷贝的概念还挺不错,所以决定迁移过来,至于从零手写一个实用的深拷贝,我决定再开一篇文章,那么本文开始。
贰 ❀ 深拷贝与浅拷贝的区别
如何区分深拷贝与浅拷贝,简单点来说,就是假设B复制了A,当修改A时,看B是否会发生变化,如果B也跟着变了,说明这是浅拷贝,拿人手短;如果B没变,那就是深拷贝,自食其力。
此篇文章中也会简单阐述到栈堆,基本数据类型与引用数据类型,因为这些概念能更好的让你理解深拷贝与浅拷贝。
我们来举个浅拷贝例子:
let a=[0,1,2,3,4],
b=a;
console.log(a===b);
a[0]=1;
console.log(a,b);
嗯?明明b复制了a,为啥修改数组a,数组b也跟着变了,这里我不禁陷入了沉思。那么这里,就得引入基本数据类型与引用数据类型的概念了。
叁 ❀ 基本数据类型与引用数据类型
基本数据类型有哪些?大家熟悉的可能有number,string,boolean,null,undefined,symbol,BigInt(任意精度整数)七类。
引用数据类型也非常多,比如对象{}
,数组[]
,函数fn
,正则等等。
而这两类数据存储分别是这样的:
基本类型--名值存储在栈内存中,例如let a = 1
;
当你执行复制let b = a
时,栈内存会开辟一个新内存,比如这样:
多以当你此时修改a = 2
时,对b
并不会造成影响,因为此时的b
已自食其力,翅膀硬了,不受a
的影响了。当然,let a = 1, b = a;
虽然b
不受a
影响,但这也算不上深拷贝,因为深拷贝本身只针对较为复杂的object
类型数据。
我们再看来看看引用类型数据的存储情况,比如let a = [1,2,3]
,此时变量名存在栈内存中,值存在于堆内存中,但是栈内存会提供一个引用的地址指向堆内存中的值,我们以上面浅拷贝的例子画个图:
图4
而当执行let b = a
时,其实复制的是a
的引用地址,并非堆里面具体的值。
因此假设此时执行a[0] = 2
对数组进行修改,由于a
与b
指向的是同一个地址,所以b
自然也受到了影响,这就是所谓的浅拷贝了。
那要是在堆内存中也开辟一个新的内存专门为b
存放值,就像基本类型那样,岂不就达到深拷贝的效果了。
肆 ❀ 实现简单的深拷贝(均为乞丐版)
写在前面,以下的实现均为简单的实现,并适合实际业务开发,像loadsh
三方工具库均有提供像深拷贝常用的工具函数,下面的实现作为了解即可,较为强大的手写深拷贝我会单独出一篇文章。
肆 ❀ 壹 JSON.parse与stringify
原理其实很简单,先将对象转成JSON
字符串后并复制一份,再通过stringfy
还原成对象,依次达到深拷贝的目的:
function deepClone(obj) {
// 转JSON字符串并复制
let _obj = JSON.stringify(obj),
// 还原成对象
objClone = JSON.parse(_obj);
return objClone
}
let a = [0, 1, [2, 3], 4],
b = deepClone(a);
a[0] = 1;
a[2][0] = 1;
console.log(a, b);
可以看到,这下b是完全不受a的影响了,但这个方法有个大问题,对于undefined
或者函数在深拷贝的过程中均会造成丢失,比如:
let a = {
a: undefined,
b: 1,
c: null,
d: function () {}
},
// 使用上面我们实现的深拷贝
b = deepClone(a);
console.log(a, b);
肆 ❀ 贰 递归实现简单的深拷贝
我们假定有一个这样的对象,那是不是在拷贝前,我们自定义一个空对象,然后判断每个key
对应的value
,看value
是不是一个对象或者数组,如果是对象/数组,我们需要递归继续拷贝,如果不是则可以直接赋值,以此来模拟拷贝过程:
const obj = {
name: 1,
user: {
userName: 'echo',
class: [1, 2, 3],
},
fn: function(){}
}
const deepClone = (obj) => {
// 这里只考虑了object类型,基础类型或者其它类型直接返回
if (typeof obj !== 'object') {
return obj;
};
const obj_ = Array.isArray(obj) ? [] : {};
for (let k in obj) {
if (obj.hasOwnProperty(k)) {
if (typeof obj[k] === 'object') {
obj_[k] = deepClone(obj[k])
} else {
obj_[k] = obj[k];
}
}
}
return obj_;
}
const obj_ = deepClone(obj);
obj.user.class[0] = 2;
console.log(obj, obj_);
可以看到不管对象属性嵌套多深,在拷贝完成后修改属性,都不会对新对象造成影响,当然此方法距离生产可用仍然有一些差距。
伍 ❀ 识别数组中浅拷贝方法
在实际开发中,我们常常用到数组中一些会返回新数组的api
,比如slice
,此方法可用于裁剪一定范围的元素,并返回一个新数组:
const a = [[1, 2], 2, 3, 4];
const b = a.slice(0);
a[0][0] = 2;
console.log(a, b);// [2, 2, 3, 4] [1, 2, 3, 4]
有人一看,哎,这明显是深拷贝啊,我改了原数组,对于b
并未造成任何影响。
但事实上,slice
在拷贝数组时,是一个接一个元素进行复制,这个数组中每个元素都是数字,基本类型,你赋值过去当然不会造成影响,假设我们将元素改成数组,比如:
const a = [[1, 2], 2, 3, 4];
const b = a.slice(0);
a[0][0] = 2;
console.log(a, b);// [[2, 2], 2, 3, 4] [[2, 2], 2, 3, 4]
可以看到,由于第一个元素是数组,slice
在拷贝时依旧是拿了数组的引用地址,改一个还是会影响另一方。
除此之外,像concat
,拓展运算符...
,filter
一样有这样的问题:
const a = [1, 2, [1, 2]];
const b = [...a]
const c = [].concat(a);
a[2][1] = 3;
console.log(a, b, c);// [1, 2, [1, 3]] [1, 2, [1, 3]] [1, 2, [1, 3]]
const a = [1, 2, 3, [1, 2]];
const b = a.filter(e => typeof e !== 'number');
a[3][0] = 5
console.log(a, b);// [1, 2, 3, [5, 2]] [5, 2]
所以在日常数组操作时,假设有深拷贝的场景,这一点一定要注意甄别。
陆 ❀ 总
那么到这里,我们简单普及了深拷贝与浅拷贝的区别,以及简单的实现了一个属于自己的深拷贝。除此之外,我们特意提及了数组api
中一些看似深拷贝的数组api
,也便于大家在开发中更安全的去处理引用数据类型的复制。
更完美的深拷贝手写,我们在下篇文章见,那么本文到这里结束。
转载自:https://juejin.cn/post/7134969919724060686