前后端面试题库 (面试必备) 推荐:★★★★★
地址:前端面试题库 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 和 port2 const { port1, port2 } = new MessageChannel(); // 将要拷贝的对象通过 port1 发送出去 port1.postMessage(obj); // 监听 port2 收到的消息 port2.onmessage = (msg) => { // 当 port2 收到消息时,将消息的数据作为 Promise 的结果进行 resolve resolve(msg.data); } }) } deepCopy(obj).then(res => { console.log(res); })
六、最后的话
深拷贝和浅拷贝不存在什么优劣、高级低级之分,在不同的需求场景使用合适的方法即可。
能力一般,水平有限,本文可能存在纰漏或错误,如有问题欢迎大佬指正,感谢你阅读这篇文章,如果你觉得写得还行的话,不要忘记点赞、评论、收藏哦!祝大家生活愉快!