在聊深拷贝和浅拷贝之前,我们先来看看以下这几个场景:
let obj1 = {
a: 1,
b: 2
};
let obj2 = obj1;
obj2.a = 3;
console.log(obj1.a); //3
let a = "ceshi";
let b = a;
b = "ceshi1";
console.log(a, b); // ceshi,ceshi1
在上面的两个例子中,我们可以看到第一个是通过对象赋值,然后修改值,发现原有的值也会进行了改变。
第二个例子中,也是通过赋值,但是修改值后,原有的值不会发生改变。这是为什么呢?再这里我们就需要知道js中基本类型和引用类型的概念。
基本类型和引用类型
mdn中是这样定义
ECMAScript变量可能包含两种不同数据类型的值:基本类型值和引用类型值。基本类型值指的是那些保存在栈内存中的简单数据段,即这种值完全保存在内存中的一个位置。而引用类型值是指那些保存堆内存中的对象,意思是变量中保存的实际上只是一个指针,这个指针指向内存中的另一个位置,该位置保存对象。
或许看文字还是挺难理解,那么我们可以看下以下的图例来帮助你更好的理解引用类型和基本类型。
在上面图例中表示,obj1开了一个空间,然后直接赋值于obj2那么它们两个所属是同时指向于同一个空间,那么obj2的改变,由于obj1指向的空间相同,那么obj1的值也会发生改变。
那么由此可知:
引用类型的值同时存储在栈内存和堆内存中,栈内存中保存的是变量名和指向堆内存中对象的指针。造成这样的原因是由于在Js中不允许直接操作对象的内存空间,只能操作对这个对象的引用
由此可知:
基本类型的复制,实际上是在栈内存中开辟了新的存储空间,来存储新的变量和值。一个的改变,不会影响另一个的变化,二者互不干扰。
那么,在有些场景中,我们需要对象或者是数组改变前的值,这种情况我们又怎么处理呢?所以,这里我们就需要知道浅拷贝和深拷贝了。
浅拷贝和深拷贝
可能有些朋友就想到,如果我需要数据的原始值的话,那么我可以使用vue中的watch来进行监听啊,因为它会返回newVal和oldVal。那么,我们再来看下以下的场景:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<script src="https://cdn.bootcss.com/vue/2.6.10/vue.js"></script>
</head>
<body>
<div id="app">
<button @click="add">添加</button>
</div>
<script>
new Vue({
el: "#app",
data: {
list: [{
id: 1,
name: "咖啡"
},
{
id: 2,
name: "绿茶"
}
]
},
methods: {
add() {
this.list.push({
id: 3,
name: "奶茶"
})
}
},
watch: {
list(nVal, oVal) {
console.log(nVal); // list.length = 3
console.log(oVal); // list.length = 3
}
},
})
</script>
</body>
</html>
在上面的结果中,我们可以看到新的值和旧的值一样的,vue是这样解释的:
注意:在变异 (不是替换) 对象或数组时,旧值将与新值相同,因为它们的引用指向同一个对象/数组。Vue 不会保留变异之前值的副本。
换句话来说,vue在监听引用类型的时候,是不会帮你保留原有的数值的。如果是基本类型的则会帮你保留原有的数值,大家可以自行尝试。
在我经手的一个项目中,有一个功能是"撤销",也就是"crtl+z"的操作,是需要缓存起原有的对象,但是我通过watch是不能获取对象的旧值,那么这个时候就需要使用到深拷贝了。
Array
实现深拷贝我们有以下几种方式
使用es6中的数组结构 …
也可以使用数组方法slice()
var arr1 = [1, 2], arr2 = arr1.slice();
console.log(arr1); //[1, 2]
console.log(arr2); //[1, 2]
arr2[0] = 3; //修改arr2
console.log(arr1); //[1, 2]
console.log(arr2); //[3, 2]
此时,arr2的修改并没有影响到arr1,看来深拷贝的实现并没有那么难嘛。我们把arr1改成二维数组再来看看:
var arr1 = [1, 2, [3, 4]], arr2 = arr1.slice();
console.log(arr1); //[1, 2, [3, 4]]
console.log(arr2); //[1, 2, [3, 4]]
arr2[2][1] = 5;
console.log(arr1); //[1, 2, [3, 5]]
console.log(arr2); //[1, 2, [3, 5]]
这个时候,坑爹的事情又发生了,arr2的值又改变了arr1的值,看来slice()只能实现一维数组的深拷贝。具备同等特性的还有:concat、Array.from() 。
Object
如果是想合并对象的话,往往会使用es6中object.assign()
var obj1 = {x: 1, y: 2}, obj2 = Object.assign({}, obj1);
console.log(obj1) //{x: 1, y: 2}
console.log(obj2) //{x: 1, y: 2}
obj2.x = 2; //修改obj2.x
console.log(obj1) //{x: 1, y: 2}
console.log(obj2) //{x: 2, y: 2}
美滋滋,原有的数据又可以获取到了,再来看下以下情况:
var obj1 = {
x: 1,
y: {
m: 1
}
};
var obj2 = Object.assign({}, obj1);
console.log(obj1) //{x: 1, y: {m: 1}}
console.log(obj2) //{x: 1, y: {m: 1}}
obj2.y.m = 2; //修改obj2.y.m
console.log(obj1) //{x: 1, y: {m: 2}}
console.log(obj2) //{x: 2, y: {m: 2}}
这时候发现,obj2的改变又影响到了obj1,Object.assign()也只能实现一维对象的深拷贝。
JSON.parse(JSON.stringify(obj))
var obj1 = {
x: 1,
y: {
m: 1
}
};
var obj2 = JSON.parse(JSON.stringify(obj1));
console.log(obj1) //{x: 1, y: {m: 1}}
console.log(obj2) //{x: 1, y: {m: 1}}
obj2.y.m = 2; //修改obj2.y.m
console.log(obj1) //{x: 1, y: {m: 1}}
console.log(obj2) //{x: 2, y: {m: 2}}
JSON.parse(JSON.stringify(obj)) 看起来很不错,不过MDN文档 的描述有句话写的很清楚:
undefined、任意的函数以及 symbol 值,在序列化过程中会被忽略(出现在非数组对象的属性值中时)或者被转换成 null(出现在数组中时)。
var obj1 = {
x: 1,
y: undefined,
z: function add(z1, z2) {
return z1 + z2
},
a: Symbol("foo")
};
var obj2 = JSON.parse(JSON.stringify(obj1));
console.log(obj1) //{x: 1, y: undefined, z: ƒ, a: Symbol(foo)}
console.log(JSON.stringify(obj1)); //{"x":1}
console.log(obj2) //{x: 1}
发现,在将obj1进行JSON.stringify()序列化的过程中,y、z、a都被忽略了,也就验证了MDN文档的描述。既然这样,那JSON.parse(JSON.stringify(obj))的使用也是有局限性的,不能深拷贝含有undefined、function、symbol值的对象,不过JSON.parse(JSON.stringify(obj))简单粗暴,已经满足90%的使用场景了。
经过验证,我们发现JS 提供的自有方法并不能彻底解决Array、Object的深拷贝问题。只能祭出大杀器:递归
function deepCopy(obj) {
// 创建一个新对象
let result = {}
let keys = Object.keys(obj),
key = null,
temp = null;
for (let i = 0; i < keys.length; i++) {
key = keys[i];
temp = obj[key];
// 如果字段的值也是一个对象则递归操作
if (temp && typeof temp === 'object') {
result[key] = deepCopy(temp);
} else {
// 否则直接赋值给新对象
result[key] = temp;
}
}
return result;
}
var obj1 = {
x: {
m: 1
},
y: undefined,
z: function add(z1, z2) {
return z1 + z2
},
a: Symbol("foo")
};
var obj2 = deepCopy(obj1);
obj2.x.m = 2;
console.log(obj1); //{x: {m: 1}, y: undefined, z: ƒ, a: Symbol(foo)}
console.log(obj2); //{x: {m: 2}, y: undefined, z: ƒ, a: Symbol(foo)}
最后做一个小小的总结:
浅拷贝:就是拷贝一层,面对深层次的对象级别,就仅仅拷贝了其引用。
深拷贝:就是拷贝多层,即对每一层的数据都进行了拷贝,面对深层次对象级别,防止拷贝了其引用类型