js 深拷贝和浅拷贝
深浅拷贝对比
深拷贝和浅拷贝是只针对Object和Array这样的对象数据类型的。
深拷贝和浅拷贝的示意图大致如下:
基本类型--名值存储在栈内存中,例如let a=1;
当b=a复制的时候,栈内存会新开辟一个内存,如下:
当你修改a=2时,b并不会收到影响,因为他有了自己独立的内存空间,但这并不是深拷贝,深拷贝和浅拷贝是只针对Object和Array这样的对象数据类型的。
引用数据类型--名存在栈内存中,值存在于堆内存中,但是栈内存会提供一个引用的地址指向堆内存中的值
当进行b=a拷贝时。其实是复制了a的引用地址,而并非是堆内存里面的值,
当我们a[0]=1时进行数组修改时,由于a与b指向的是同一个地址,所以自然b也受了影响,这就是所谓的浅拷贝了。
那,要是在堆内存中也开辟一个新的内存专门为b存放值,就像基本类型那样,起步就达到深拷贝的效果了
浅拷贝只是复制了指向某个对象的指针,并没有复制对象的值,新旧对象还是共享同一个内存空间,但深拷贝会重新创建一个相同的对象,新对象有自己的指针和内存空间。我们可以多次使用同样的数据,数据修改后也不会发生错乱。
浅拷贝的实现方式
1. 直接赋值
let obj = {username: 'kobe', age: 39, sex: {option1: '男', option2: '女'}};
let obj1 = obj;
obj1.sex.option1 = '不男不女'; // 修改复制的对象会影响原对象
console.log(obj1, obj);
2.Object.assign()
let obj = {
username: 'kobe'
};
let obj2 = Object.assign(obj);
obj2.username = 'wade';
console.log(obj);//{username: "wade"}
3.Array.prototype.concat()
let arr = [1, 3, {
username: 'kobe'
}];
let arr2=arr.concat();
arr2[2].username = 'wade';
console.log(arr);
4.Array.prototype.slice()
let arr = [1, 3, {
username: ' kobe'
}];
let arr3 = arr.slice();
arr3[2].username = 'wade'
console.log(arr);
关于Array的slice和concat方法的补充说明:Array的slice和concat方法不修改原数组,只会返回一个浅复制了原数组中的元素的一个新数组。
- 如果该元素是个对象引用(不是实际的对象),slice 会拷贝这个对象引用到新的数组里。两个对象引用都引用了同一个对象。如果被引用的对象发生改变,则新的和原来的数组中的这个元素也会发生改变
- 对于字符串、数字及布尔值来说(不是 String、Number 或者 Boolean 对象),slice 会拷贝这些值到新的数组里。在别的数组里修改这些字符串或数字或是布尔值,将不会影响另一个数组。
let arr = [1, 3,[5,8] {
username: ' kobe'
}];
let arr3 = arr.slice();
arr3[1] = 2
console.log(arr,arr3);
对于这种浅层次的确实不受影响,但是深层次的呢?我们来看一看
let arr = [1, 3,[5,8], {
username: ' kobe'
}];
let arr4 = arr.concat();
arr4[1] = 2
arr4[2][0] = 9
console.log(arr,arr4);
可以看到这种二级属性还是没能拷贝成功。所以slice和concat方法并不是真正的深拷贝方法。
深拷贝的实现方式
1、递归
递归方法实现深度克隆原理:遍历对象、数组直到里边都是基本数据类型,然后再去复制,就是深度拷贝
// 定义一个深拷贝函数 接收目标target参数
function deepClone(target) {
// 定义一个变量
let result;
// 如果当前需要深拷贝的是一个对象的话
if (typeof target === 'object') {
// 如果是一个数组的话
if (Array.isArray(target)) {
result = []; // 将result赋值为一个数组,并且执行遍历
for (let i in target) {
// 递归克隆数组中的每一项
result.push(deepClone(target[i]))
}
// 判断如果当前的值是null的话;直接赋值为null
} else if(target===null) {
result = null;
// 判断如果当前的值是一个RegExp对象的话,直接赋值
} else if(target.constructor===RegExp){
result = target;
}else {
// 否则是普通对象,直接for in循环,递归赋值对象的所有值
result = {};
for (let i in target) {
result[i] = deepClone(target[i]);
}
}
// 如果不是对象的话,就是基本数据类型,那么直接赋值
} else {
result = target;
}
// 返回最终结果
return result;
}
let obj1 = {
a: {
c: /a/,
d: undefined,
b: null
},
b: function () {
console.log(this.a)
},
c: [
{
a: 'c',
b: /b/,
c: undefined
},
'a',
3
]
}
let obj2 = deepClone(obj1);
obj2.c[0].a='aaaa'
console.log(obj1,obj2);
可以看到obj里面的全部属性都拷贝成功,修改obj2,obj1的数据并不会受影响
2、JSON.parse(JSON.stringify())
let arr = [1, 2, {
username: ' lihh'
}];
let arr4 = JSON.parse(JSON.stringify(arr));
arr4[2].username = 'whhh';
console.log(arr, arr4)
原理: 用JSON.stringify将对象转成JSON字符串,再用JSON.parse()把字符串解析成对象,一去一来,新的对象产生了,而且对象会开辟新的栈,实现深拷贝。
这种方法虽然可以实现数组或对象深拷贝,但不能处理函数
let arr = [1, 2, {
username: ' lihh'
},function(){}];
let arr4 = JSON.parse(JSON.stringify(arr));
arr4[2].username = 'whhh';
console.log(arr, arr4)
因为JSON.stringify() 方法是将一个JavaScript值(对象或者数组)转换为一个 JSON字符串,不能接受函数
缺点:如果对象中包含时间格式就会转为字符串、如果包含正则就会成为 undefined、如果包含function、undefined就会丢失。