前后端面试题库 (面试必备) 推荐:★★★★★
地址:前端面试题库 web前端面试题库 VS java后端面试题库大全
一、引言
当我们需要在 JavaScript 中处理对象和数组时,经常需要使用对象和数组的复制功能。JS中有着两种复制方式:深拷贝和浅拷贝。两种方式的复制效果不同,适用场景也不同。
二、什么是浅拷贝和深拷贝?
1.浅拷贝
浅拷贝就是对对象或数组的第一层进行复制,如果这个属性是基本类型数据则直接复制,如果是引用类型数据则只是浅复制一份引用(内存地址),这个引用指向的是原有的引用类型数据。这就意味着,如果复制得到的数据被修改,原有的引用类型数据也会受到影响
。
2.深拷贝
在 JavaScript 中,深拷贝是指将一个对象或数组完全复制一份,生成一份新的,不管有多少层嵌套关系都要完全独立出来。也就是说,深拷贝实现的是真正意义上的复制而不是一种引用。如果复制后的对象或数组被修改,原来的对象或数组也不会受到影响
。
三、浅拷贝和深拷贝的区别
使用浅拷贝方式得到的新对象和原对象共享引用类型的数据,因此如果修改新对象中的引用类型数据,原对象也会受到影响,而深拷贝会完全复制一个对象,新对象与原对象间没有任何关系,因此任何修改新对象中的引用类型数据,都不会影响原对象。因此,在处理嵌套数据结构的情况下,深拷贝比浅拷贝更为可靠。
四、实现浅拷贝和深拷贝的方法
1.浅拷贝
1-1.slice()
Array.prototype.slice()方法可以将数组中的一部分元素复制到一个新的数组中,这个方法是浅拷贝,因为它只复制对象的引用而不是对象本身。可以看到,修改了复制对象arr2的值后,原有对象arr1的值也被改了。
let arr1 = [1, 2, { a: 3, b: {c: 4}}];
let arr2 = arr1.slice();
console.log(arr1); //[ 1, 2, { a: 3, b: { c: 4 } } ]
console.log(arr2); //[ 1, 2, { a: 3, b: { c: 4 } } ]
arr2[2].b.c = 666;
let arr3 = arr1.slice();
console.log(arr1); //[ 1, 2, { a: 3, b: { c: 666 } } ]
console.log(arr3); //[ 1, 2, { a: 3, b: { c: 666 } } ]
1-2.concat()
Array.prototype.concat()方法可以将数组中的一部分元素复制到一个新的数组中,也可以将多个数组合并成一个新数组。这个方法是浅拷贝,因为它只复制对象的引用而不是对象本身。可以看到,修改了复制对象arr2的值后,原有对象arr1的值也被改了。
let arr1 = [1, 2, { a: 3, b: {c: 4}}];
let arr2 = [].concat(arr1);
// let arr2 = arr1.concat();
console.log(arr1); //[ 1, 2, { a: 3, b: 4 } ]
console.log(arr2); //[ 1, 2, { a: 3, b: 4 } ]
arr2[2].b.c = 666;
let arr3 = arr1.concat();
console.log(arr1); //[ 1, 2, { a: 3, b: { c: 666 } } ]
console.log(arr3); //[ 1, 2, { a: 3, b: { c: 666 } } ]
1-3.Object.assign()
Object.assign()方法可以将多个对象的属性进行浅复制,浅复制只是复制对象的引用,而不是对象本身。可以看到,修改了复制对象obj2的值后,原有对象obj1的值也被改了。
let obj1 = { a: 1, b: {c: 2}};
let obj2 = Object.assign({}, obj1);
console.log(obj1); //{ a: 1, b: { c: 2 } }
console.log(obj2); //{ a: 1, b: { c: 2 } }
console.log(obj2.b); //{ c: 2 }
obj2.b.c = 666;
console.log(obj1); //{ a: 1, b: { c: 666 } }
console.log(obj2); //{ a: 1, b: { c: 666 } }
console.log(obj2.b); //{ c: 666 }
1-4.Object.create()
Object.create()方法可以将一个对象作为原型,创建一个新的对象。新的对象是浅拷贝原型对象的属性,也就是只复制对象的引用而不是对象本身。可以看到,修改了复制对象obj2的值后,原有对象obj1的值也被改了。
let obj1 = { a: 1, b: {c: 2}};
let obj2 = Object.create(obj1);
console.log(obj1); //{ a: 1, b: { c: 2 } }
console.log(obj2); //{}
console.log(obj2.a); //1
console.log(obj2.b); //{ c: 2 }
obj2.b.c = 666;
console.log(obj1); //{ a: 1, b: { c: 666 } }
console.log(obj2); //{}
console.log(obj2.a); //1
console.log(obj2.b); //{ c: 666 }
Object.create()方法创建一个新对象,新对象的原型是指定的对象。新对象继承了参数对象的属性,但是并没有属性和方法,所以看起来是个空对象,所以严格来说可能不是浅拷贝,这个方法仅供参考
。
1-5.扩展运算符(...)
扩展运算符可以将一个对象展开成多个单独的属性,相当于浅拷贝,也是复制对象引用而不是对象本身。可以看到,修改了复制对象obj2的值后,原有对象obj1的值也被改了。
let obj1 = { a: 1, b: {c: 2}};
let obj2 = { ...obj1 };
console.log(obj1); //{ a: 1, b: { c: 2 } }
console.log(obj2); //{ a: 1, b: { c: 2 } }
console.log(obj2.b); //{ c: 2 }
obj2.b.c = 666;
console.log(obj1); //{ a: 1, b: { c: 666 } }
console.log(obj2); //{ a: 1, b: { c: 666 } }
console.log(obj2.b); //{ c: 666 }
2.深拷贝
2-1.JSON.parse(JSON.stringify())
这种方式的实现是先将对象转换成JSON字符串,再将JSON字符串转换回对象,这样就可以完全复制对象或数组,同时所有数据都是基本类型数据,不存在引用类型数据的互相影响的问题。
let obj = {fruit: '水果',type: {one: {name: '哈密瓜',price: 10},two: {name: '西瓜',price: 20,date: new Date(),regexp: /^B/,birth: undefined},three: {name: Symbol("荔枝"),price: 30}}
};
let obj2 = JSON.parse(JSON.stringify(obj));
console.log(obj2);
// {
// fruit: '水果',
// type: {
// one: { name: '哈密瓜', price: 10 },
// two: {
// name: '西瓜',
// price: 20,
// date: '2023-06-14T02:50:07.911Z',
// regexp: {}
// },
// three: { price: 30 }
// }
// }
obj2.type.one.name = "蓝莓"; //修改拷贝对象,看原对象会不会改变
console.log(obj);
// {
// fruit: '水果',
// type: {
// one: { name: '哈密瓜', price: 10 },
// two: {
// name: '西瓜',
// price: 20,
// date: 2023-06-14T02:52:16.530Z,
// regexp: /^B/,
// birth: undefined
// },
// three: { name: Symbol(荔枝), price: 30 }
// }
// }
但是需要注意的是,这种方式有缺陷
。
- 无法复制
函数
和RegExp正则表达式
等特殊对象 - 无法处理
循环引用
的情况 - 无法复制
undefined
和symbol
类型的属性 - 对象中的
Date
类型会被转换成字符串 - 对象中含有
NaN
,Infinity
会变成null
五、手写实现深拷贝和浅拷贝
1.浅拷贝
// 简陋版浅拷贝
function shallowCopy(obj) {// 判断是否是对象或者数组if (typeof obj !== 'object' || obj === null) {return obj;}// 判断当前属性是数组还是对象let newObj = Array.isArray(obj) ? [] : {};for (let key in obj) {if (obj.hasOwnProperty(key)) {// 复制属性值newObj[key] = obj[key];}}return newObj;
}
2.深拷贝
2-1 方法一
// 简陋版深拷贝
function deepCopy(obj) {// 如果obj是null,则直接返回if(obj === null){return null;}// 如果obj不是对象或数组,则直接返回if(typeof obj !== 'object'){return obj;}if (obj instanceof RegExp) return new RegExp(obj);// 处理正则表达式if (obj instanceof Date) return new Date(obj); // 处理日期对象// 判断obj是数组还是对象let newObj = Array.isArray(obj) ? [] : {};// 遍历对象或数组的所有属性或元素// 判断是否是显示具有的属性,而不是从原型上继承得到的属性for(let key in obj){if(Object.prototype.hasOwnProperty.call(obj, key)) {// Reflect.ownKeys(obj)// 如果属性或元素还是对象或数组,则递归调用深拷贝函数newObj[key] = typeof obj[key] === 'object' ? deepCopy(obj[key]) : obj[key];}}return newObj;
}
2-2 方法二
MessageChannel接口允许我们创建一个新的消息通道,并通过它的两个MessagePort 属性发送数据。
function deepCopy(obj) {return new Promise((resolve, reject) => {// 创建一个新的 MessageChannel 对象,并获取两个端口 port1 和 port2const { port1, port2 } = new MessageChannel(); // 将要拷贝的对象通过 port1 发送出去port1.postMessage(obj); // 监听 port2 收到的消息port2.onmessage = (msg) => { // 当 port2 收到消息时,将消息的数据作为 Promise 的结果进行 resolveresolve(msg.data); }})
}
deepCopy(obj).then(res => {console.log(res);
})
六、最后的话
深拷贝和浅拷贝不存在什么优劣、高级低级之分,在不同的需求场景使用合适的方法即可。
能力一般,水平有限,本文可能存在纰漏或错误,如有问题欢迎大佬指正,感谢你阅读这篇文章,如果你觉得写得还行的话,不要忘记点赞、评论、收藏哦!祝大家生活愉快!
前后端面试题库 (面试必备) 推荐:★★★★★
地址:前端面试题库 web前端面试题库 VS java后端面试题库大全