在JS中,数据类型分为基本数据类型和引用数据类型两种,对于基本数据类型来说,它的值直接存储在栈内存中,而对于引用类型来说,它在栈内存中仅仅存储了一个引用,而真正的数据存储在堆内存中。
1. 当我们对数据进行操作的时候,会发生两种情况
基本数据类型:
let a = 3;
let b = a;
b = 5;
console.log(a); // 3
console.log(b); // 5
可以看到的是对于基本类型来说,我们将一个基本类型的值赋予 a 变量,接着将 a 的值赋予变量 b ; 然后我们修改 b ;可以看到 b 被修改了,而 a 的值没有被修改,两个变量都使用的是独立的数据;
引用数据类型:
let obj1 = {a: 1,b: 2,c: 3}
let obj2 = obj1;
obj2.a = 5;
console.log(obj1.a); // 5
console.log(obj2.a); // 5
可以看到的是,两个对象的值全部被修改了。
对象是引用类型的值,对于引用类型来说,我们将 obj1 赋予 obj2 的时候,我们其实仅仅只是将 obj1 存储在栈堆中的的引用赋予了 obj2 ,而两个对象此时指向的是在堆内存中的同一个数据,所以当我们 修改任意一个值的时候,修改的都是堆内存中的数据,而不是引用,所以只要修改了,同样引用的对象的值也自然而然的发生了改变。
2. 为什么要使用深拷贝
我们希望在改变新的数组(对象)的时候,不改变原数组(对象)
3. 深拷贝
深拷贝作用在引用类型上!例如:Object,Array
深拷贝不会拷贝引用类型的引用,而是将引用类型的值全部拷贝一份,形成一个新的引用类型,这样
就不会发生引用错乱的问题,使得我们可以多次使用同样的数据,而不用担心数据之间会起冲突。
4. 实现一层深拷贝(浅拷贝)
4.1 concat
//数组
let arr1 = [10,20,30]
//concat(数组元素,数组,...):拼接数组元素,不改变原数组,返回新数组
let arr2 = arr1.concat()
4.2 slice
//数组
let arr1 = [10,20,30]
//slice(start,end):截取数组,含头不含尾,不改变原数组,返回新数组
let arr2 = arr1.slice(0)
4.3 扩展运算符
//数组和对象
let obj1 = {
name: "张三",
age: 18,
friend: {
name: "李四"
}
}
//一层深拷贝
let obj2 = { ...obj1 }
obj1.name = '李素';
console.log(obj1.name);//李素
console.log(obj2.name);//张三
obj1.friend.name = "王五";
console.log(obj1.friend.name);//王五
console.log(obj2.friend.name);//王五
4.4 for-in
//数组和对象
let obj1 = {
name: "张三",
age: 18,
friend: {
name: "李四"
}
}
//一层深拷贝
function clone(obj) {
let o = {}
for (const key in obj) {
o[key] = obj[key]
}
return o
}
let obj2 = clone(obj1)
obj1.name = '李素';
console.log(obj1.name);//李素
console.log(obj2.name);//张三
obj1.friend.name = "王五";
console.log(obj1.friend.name);//王五
console.log(obj2.friend.name);//王五
5. 如何进行多层深拷贝
5.1 使用JSON.stringify()以及JSON.parse()
let _tem = JSON.stringify(obj);//将对象装换为json字符串形式
let result = JSON.parse(_tem);//将转换而来的字符串转换为原生js对象
let obj1 = {
a: 10,
b: 20
}
let str = JSON.stringify(obj1);
let obj2 = JSON.parse(str);
obj2.a = 100;
console.log(obj1);
console.log(obj2);
可以看到没有发生引用问题,修改obj2的数据,并不会对obj1造成任何影响
但是使用JSON.stringify()以及JSON.parse()它是不可以拷贝 undefined , function, RegExp 等等 类型的
5.2 递归
递归:在函数内部调用自己,必须有结束递归的条件。
递:一层一层递进去(调用函数)
归:一层一层归出来(函数返回值)
使用递归的方式实现数组、对象的深拷贝
// 使用递归的方式实现数组、对象的深拷贝
function deepClone(data) {
//判断是否是对象
if (typeof data === 'object' && data !== null) {
//判断是否是数组
let res = Array.isArray(data) ? [] : {};
//读取对象或者数组的属性
for (const key in data) {
//判断是否包含某个属性,不统计原型上的属性
if (data.hasOwnProperty(key)) {
res[key] = deepClone(data[key])
}
}
//返回最终结果
return res;
} else {
return data;
}
}
// 对象
let obj1 = {
name: "张三",
friend: {
name: "李四"
}
}
let obj2 = deepClone(obj1);
obj1.friend.name = "王五";
console.log(obj1);
console.log(obj2);
// 数组
let arr1 = [10, { age: 18 }]
let arr2 = deepClone(arr1);
arr1[1].age = 100;
console.log(arr1);
console.log(arr2);
5.3 lodash
let obj1 = {
name: "张三",
friend: {
name: "李四"
}
}
// 深拷贝:_.cloneDeep(value) value: 要深拷贝的值 返回拷贝后的值。
var obj2 = _.cloneDeep(obj1);
obj2.friend.name = "王五";
console.log(obj1.friend.name);//李四
console.log(obj2.friend.name);//王五
总结:
如果一层深拷贝:
对象:扩展运算符、for-in遍历
数组:扩展运算符、for-in遍历、slice、concat等方法。
如果多层深拷贝:
第一种方法:可以使用JSON.stringify()和JSON.parse()对数据进行转换,但是这种方法有弊端,如果数据中有函数、undefined等不可以转换。
第二种方法:自己手写递归函数、思路:判断数据是否是对象或者数组,但是排除null,如果是可以再判断数据的类型是数组还是对象,然后遍历数组,拿到数据在递归判断。
第三种方法:其实我工作中使用lodash库多一些,使用_.cloneDeep(value),如果项目中已经使用lodash库,可以直接使用里面的方法,如果项目一次也没有引用过lodash库,只单纯的想使用一下lodash里面封装的深拷贝的方法,引入一个库,不划算,就可以自己封装递归函数。