在Javascript中深拷贝和浅拷贝的使用场景及二者的区别
1、使用场景
- 概述:深浅拷贝,都是拷贝对象内的数据到新对象,新旧对象内的数据相同。
- 在js中,当我们需要,使用某个对象内的数据,但是不想修改原本的对象内的数据内容时,通常会使用深浅拷贝来复制数据,并对新的对象内的数据进行操作。例如在完成修改操作时,我们会首先拿到服务器给我们返回的数据,对于这这些后台给我们的数据,我们不应该直接操作,这样会出现一个问题,就是在修改页面时,点了取消按钮,由于Vue的双向绑定的特性,在视图上也会修改数据,所以为了避免这个问题,我们通常可以采用深拷贝或者浅拷贝的方式来解决。下面分别介绍深拷贝和浅拷贝。
2、JS中的数据类型
- Number
- String
- Boolean
- Null
- undefined
- Symbol
- Object
前面6种类型是简单数据类型,而Object是引用数据类型;简单数据类型没有子类型了,不可以再进行拆分了,而复杂数据类型还有子类型,比如Array,Function,RegExp,Date等对象;正是因为这些子类型的不同导致了深拷贝的各种问题;数据类型的不同,会导致在内存中的存储方式的不同,如果是简单数据类型,存储在栈空间中,存储的是一个值;如果是复杂数据类型,存储在堆空间中,栈空间存储的是一个引用。
正是这种存储方式的差异,导致了浅拷贝和深拷贝的区别。
3、什么是深拷贝和浅拷贝?
- 浅拷贝是创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本
类型的值,如果属性是引用类型,拷贝的就是内存地址,所以如果其中一个对象改变了这个地址,就会影响到另一
个对象。 - 深拷贝是将一个对象从内存中完整的拷贝一份出来,从堆内存中开辟一个新的区域存放新对象,且修改新对象不会影
响原对象。
4、赋值和深/浅拷贝的区别?
- 前提:针对引用数据类型
- 当我们把一个对象赋值给一个新的变量时,赋的其实是该对象的在栈中的地址,而不是堆中的数据。也就是两个对象指向的是同一个存储空间,无论哪个对象发生改变,其实都是改变的存储空间的内容,因此,两个对象是联动的。
let obj1 = {
name: '张三',
arr: [1, [2, 3], 4]
};
let obj2 = obj1;
obj2.name = '张三三';
obj2.arr[2]= [5, 6, 7];
console.log('obj1', obj1); // obj1 {"name":"张三三","arr":[1,[2,3],[5,6,7]]}
console.log('obj2', obj2); // obj2 {"name":"张三三","arr":[1,[2,3],[5,6,7]]}
- 浅拷贝:重新在堆中创建内存,拷贝前后对象的基本数据类型互不影响,但拷贝前后对象的引用类型因共享同一块内存,会相互影响。
let obj1 = {
name: '张三',
arr: [1, [2, 3], 4]
};
let obj2 = shallowClone(obj1);
obj2.name = '张三三';
obj2[1] = [5, 6, 7]; // 新旧对象还是共享一块内存
// 浅拷贝的方法
function shallowClone(source) {
let target = {};
for (let i in source) {
if (source.hasOwnProperty(i)) {
target[i] = source[i];
}
}
return target;
}
console.log('obj1', obj1); // obj1 {"name":"张三","arr":[1,[5,6,7],4]}
console.log('obj2', obj2); // obj2 {"name":"张三三","arr":[1,[5,6,7],4]}
- 深拷贝:从堆内存中开辟一个新的区域存放新对象,对对象中的子对象进行递归拷贝,拷贝前后的两个对象互相不影响。
let obj1 = {
name: '张三',
arr: [1, [2, 3], 4]
};
let obj2 = deepClone(obj1);
obj2.name = '张三三';
obj2.arr[1] = [5, 6, 7]; // 新对象跟原型对象不共享内存
// 深拷贝方法
function deepClone(obj) {
if (obj === null) return obj;
if (obj instanceof Date) return new Date(obj);
if (obj instanceof RegExp) return new RegExp(obj);
if (typeof obj !== 'object') return obj;
let cloneObj = new obj.constructor();
for (let key in obj) {
if (obj.hasOwnProperty(key)) { // 不拷贝原型上的对象
cloneObj[key] = deepClone(obj[key]); // 递归拷贝
}
}
return cloneObj;
};
console.log('obj1', obj1) // obj1 {"name":"张三","arr":[1,[2,3],4]}
console.log('obj2', obj2) // obj2 {"name":"张三三","arr":[1,[5,6,7],4]}
4、JS常见的浅拷贝方式
- Object.assign
let obj1 = {
person: {
name: '刘德华',
age: 50
},
hobby: '唱歌'
}
let obj2 = Object.assign({}, obj1)
obj2.person.name = '科比'
obj2.hobby = '打篮球'
console.log(obj1) // {"person":{"name":"科比","age":50},"hobby":"唱歌"}
- 展开运算符
let obj1 = {
person: {
name: '刘德华',
age: 50
},
hobby: '唱歌'
}
let obj2 = { ...obj1 }
obj2.person.age = '55'
obj2.hobby = '拍电影'
console.log(obj1) // {"person":{"name":"刘德华","age":"55"},"hobby":"唱歌"}
- Array.prototype.concat()
let arr = [1, 2, { person: '刘德华' }]
let arr2 = arr.concat()
arr2[2].person = '周杰伦'
console.log(arr) // [1,2,{"person":"周杰伦"}]
- Array.prototype.slice()
let arr = [1, 2, { person: '刘德华' }]
let arr2 = arr.slice()
arr2[2].person = '周杰伦'
console.log(arr) // [1,2,{"person":"周杰伦"}]
5.JS中运用序列化和反序列化可以完全实现深拷贝吗?
序列化与反序列化
- 利用JSON.stringify将对象转成JSON字符串,再用JSON.parse把字符串解析成对象
- 一来一去,新的对象产生了,而且对象会开辟新的栈,实现深拷贝
let arr = [1, 2, { person: '刘德华' }]
let arr2 = JSON.parse(JSON.stringify(arr))
arr2[2].person = '周杰伦'
console.log(arr) // [1,2,{"person":"刘德华"}]
console.log(arr2) // [1,2,{"person":"周杰伦"}]
- 注意
- 这种方法虽然可以实现深拷贝,但由于它是依赖于JSON,因此不支持JSON的其他格式
- JSON只支持object,array,string,number,true,false,null这几种数据或者值,其他的比如函数,undefined,Date,RegExp等数据类型都不支持
- 对于它不支持的数据都会直接忽略该属性
let arr = [1, 2, { person: '刘德华' }]
let arr2 = JSON.parse(JSON.stringify(arr))
arr2[2].person = '周杰伦'
console.log(arr) // [1,2,{"person":"刘德华"}]
console.log(arr2) // [1,2,{"person":"周杰伦"}]
// 对象中不能有undefined,否则无法序列化
let obj1 = { a: undefined }
let obj2 = JSON.parse(JSON.stringify(obj1))
console.log(obj2); // {}
// 对象中不能有RegExp正则,否则无法序列化
let obj3 = {
reg: /itwangblog\d/ig
}
let obj4 = JSON.parse(JSON.stringify(obj1))
console.log(obj4) // {}
// Date类型数据会被转换为字符串类型
// 如果对象中存在Date类型的数据,会被转换成字符串,从而丢失Date的一些特性,比如时间格式化等方法
let obj5 = {
date: new Date()
}
let obj6 = JSON.parse(JSON.stringify(obj5))
console.log(obj5) // {"date":"2022-03-02T08:35:29.743Z"}
console.log(obj6) // {"date":"2022-03-02T08:35:29.743Z"}
6.手写深拷贝参考代码
function deepClone(target, cache = new Map()) { // cache用来防止递归内存溢出
// 判断:如果有值就直接返回,不需要递归拷贝了
if (cache.get(target)) {
return cache.get(target);
}
if (target instanceof Object) {
let dist;
// 判断是否是数组
if (target instanceof Array) {
dist = [];
// 判断是否是函数
} else if (target instanceof Function) {
dist = function () {
return target.call(this, ...arguments);
};
// 判断是否是正则表达式
} else if (target instanceof RegExp) {
// 创建一个新的正则
dist = new RegExp(target.source, target.flags);
// 判断是否是日期
} else if (target instanceof Date) {
dist = new Date(target);
} else {
dist = {};
}
// 将属性和拷贝后的值作为一个map
cache.set(target, dist);
for (let key in target) {
if (target.hasOwnProperty(key)) { // 不拷贝原型上的属性
dist[key] = deepClone(target[key], cache); // 递归遍历
}
}
return dist;
} else {
return target;
}
};