前言
js的深拷贝,浅拷贝问题在项目的开发过程中是频繁出现的。对于一个合格的前端开发者来说,对于深刻理解它们是必要的,并能合理使用深拷贝浅拷贝处理问题。
什么是深拷贝,什么是浅拷贝
以下是我对于它们的理解:
深拷贝:如果是对象a和对象b它们是深拷贝,那么它们不再引用统一内存地址了,也就是它们 老死不相往来了。
浅拷贝:对象a只是复制指向对象b的指针,而不复制对象本身。新旧对象它们还是共享同一内存。
好了,在脑海里大致知道有这么一个小的概念之后,我们来聊聊更深入的内存。
javascript的堆栈
堆&栈 两者都是存放临时数据的地方。
栈是先进后出的,就像一个桶,后进去的先出来,它下面本来有的东西要等其他出来之后才能出来。
堆是在程序运行时,而不是在程序编译时,申请某个大小的内存空间。即动态分配内存,对其访问和对一般内存的访问没有区别。对于堆,我们可以随心所欲的进行增加变量和删除变量,不用遵循次序。
栈区(stack) 由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。 堆区(heap) 一般由程序员分配释放,若程序员不释放,程序结束时可能由OS回收。 堆(数据结构):堆可以被看成是一棵树,如:堆排序; 栈(数据结构):一种先进后出的数据结构。
数据类型访问&&复制
** 基本数据类型:定义在栈中的,从栈当中直接存取变量的值。** 例如:var a =1;
栈内存 | |
---|---|
a | 1 |
** 基本数据类型复制:复制时,会在栈中创建一个新值,然后把值复制到为新变量分配的位置上。**
例如:var b=a;
栈内存 | |
---|---|
a | 1 |
b | 1 |
** 引用数据类型:定义在栈中的,但它的值指向堆当中的一个地址。** 例如:
let obj=Object.create(null);
obj={a:1};
** 引用数据类型复制:复制的是存储在栈中的指针,将指针复制到栈中并且为新变量分配空间。而这个指针副本和原指针指向存储在堆中的同一个对象。复制操作结束后,两个变量实际上将引用同一个对象。因此,在使用时,改变其中的一个变量的值,将影响另一个变量。** 例如:
let obj=Object.create(null);
obj={a:1};
let obj2=obj;
好了,了解了数据结构之后,那我们来谈谈如何实现深浅拷贝。
如何实现深浅拷贝
毋庸置疑,浅拷贝的实现是大多数人都能想到的,因为在项目的开发的过程当中,就是有时候因为数据浅拷贝而引发一系列的数据问题。
源数据data:
let data={
name:'ddd',
type:'es',
impl:[
{
e:1,
a:2
},
{
e:2,
a:3
}
]
}
如何实现浅拷贝
- 第一种情况:将数据源data直接赋值给另外一个对象。 猜猜下面代码结果
let obj=data; //浅拷贝
obj.name='aa' //当obj修改name的值之后,data的name值也发生改变
console.log(obj1,data);
- 第二种情况:将数据源data做为另一个对象的子对象。猜猜下面代码结果
let obj1={data}; //定义obj1是一个对象,并且将data是obj1的子对象。
obj1.name='bb' //给obj1添加name属性,data的name值不发生改变
obj1.data.impl[0].e=4; //但是修改obj的data属性的值,数据源data会发生改变
obj1.data.name='pp';
console.log(obj1,data);
- 第三种情况:使用es6的扩展符号{...XXX}。猜猜下面代码结果
let obj2={...data};
obj2.name='cc';
obj2.impl[0].e=3
console.log(obj2,data);
第三种的情况是特殊的,但是这种在JavaScript当中是常见的。有时候是深拷贝,有时候是浅拷贝,只有当第一层属性值不是对象的时候则是深拷贝否则就是浅拷贝。 总结一下有时候深拷贝有时候浅拷贝的特殊语法:
1、for in 遍历循环拷贝
2、es6 Object.assign({ },obj)
3、{… obj}
4、concat
5、slice
好吧,上面三种情况的结果依次如下:
第一种情况:
第二种情况:
第三种情况:
那如何才能实现真正的拷贝呢?也就是真的老死不相往来了,一点关系也没有了呢?
如何实现深拷贝
- 利用JSON对象的parse和stringfy
JSON.stringfy是将一个js对象值转成一个JSON字符串,JSON.parse是将一个JSON字符串转成一个js对象。先转成字符串再转对象,所生成的对象和原来的对象不共用同一内存,实现深拷贝的过程。
但是有个问题:undefined、function、symbol在转化中会被忽略。
后记:使用JSON.stringfy(参数1,参数2,参数3)进行过滤。
var foo = {
bar: "1",
baz: "3",
o: {
name: 'xiaolo',
age: 21,
info: {
sex: 'nan',
getSex: function() {
return 'nan';
}
}
}
}
var f = JSON.stringify(foo, cencer);
var c = JSON.parse(f);
console.log(c);
如下图:
- 利用递归函数来实现
实现思路就是循环对象,判断对象的属性值是否还是对象,如果还是那就递归。
function cloneData(obj){
let target;
if(typeof obj==='object' && obj!==null){
if(Array.isArray(obj)){
target=[];
}else{
target={};
}
}else{
return target=obj;
}
for(let key in obj){
if(obj.hasOwnProperty(key)){
if(typeof obj[key] ==='object'){
target[key]=cloneData(obj[key]);
}else{
target[key]=obj[key];
}
}
}
return target;
}
补充:上面的cloneData对于对象环是无法拷贝的,会栈溢出。所以考虑到对象环的问题需要修改cloneData的函数。
function checktype(obj) {
//检查对象类型
return Object.prototype.toString.call(obj).slice(8, -1)
}
function depCopy(target, hash = new WeakMap()) {
//hash 作为一个检查器,避免对象深拷贝中出现环引用,导致爆栈。
let type = checktype(target)
let result = null
if (type == 'Object') {
result = {}
} else if (type == 'Array') {
result = []
} else {
return target
}
if (hash.has(target)) {
//检查是有存在相同的对象在之前拷贝过,有则返回之前拷贝后存于hash中的对象
return hash.get(target)
}
hash.set(target, result) //备份存在hash中,result目前是空对象、数组。后面会对属性进行追加,这里存的值是对象的栈
for (let i in target) {
if (
checktype(target[i]) == 'Object' ||
checktype(target[i]) == 'Array'
) {
result[i] = depCopy(target[i], hash) //属性值是对象,进行递归深拷贝
} else {
result[i] = target[i] //其他类型直接拷贝
}
}
return result
}
- loadsh第三方库来实现 _.cloneDeep
引入第三方库,使用现成api实现深拷贝。
var obj={
a:{
b:2,
c:3
}
}
var obj1=_.cloneDeep(obj);
obj1.a.c=6;
总结
以上就是我对深拷贝浅拷贝的理解,小伙伴可以自己动手一下,如果上述的内容有帮到你,那就给我点个赞哈~感谢啦!!如有其他的想法可在评论区留言,互相学习成长~~