面试官:请你说下深、浅拷贝并且手写深、浅拷贝,我:你咋知道我只会这个?

前后端面试题库 (面试必备) 推荐:★★★★★

地址:前端面试题库  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 }
//     }
//  }

但是需要注意的是,这种方式有缺陷

  1. 无法复制函数RegExp正则表达式等特殊对象
  2. 无法处理循环引用的情况
  3. 无法复制undefinedsymbol类型的属性
  4. 对象中的Date类型会被转换成字符串
  5. 对象中含有NaNInfinity会变成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后端面试题库大全

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/19935.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

TCP 与UDP区别

目录 网络参考模型TCPTCP 是什么特点 UDPUDP 是什么特点 TUP与UDP区别 总结什么时候选TCP or UDP 网络参考模型 TCP TCP 是什么 传输控制协议(TCP)是TCP/IP模型的传输层协议。它是一个面向连接的协议。因此,协议首先在源和目标之间建立连接…

谈谈对SpringMVC的理解

1、SpringMVC是属于SpringFramework生态里面的一个模块,它是在Servelet基础上构建的,并且使用了MVC模式设计的一个Web框架; 2、它的主要目的是为了简化传统模式下的Serveletjsp的开发模式,其次SpringMVC的架构模式是对于Java的web…

AR 技术应用与管理解决方案:施工建造、机柜扫描、办公室导航

建筑行业作为人类历史上最重要的产业之一,在数字化转型方面同样也在不断推进。图扑软件结合 AR 技术的应用,为建筑行业带来了更加便捷高效的建筑施工过程管理。 传统的建筑施工管理过程中,由于缺乏信息化手段,往往存在资料不全、…

面试算法题—编程题2023

面试算法题—编程题2023 一、冒泡排序二、选择排序三、快速排序四、其它排序 最近在看一些面试题,发现很多面试过程中都会要求手写排序编程题,经过一番查找整理,可以快速学习和使用相关排序算法题,通俗易懂,手撕代码吊…

RFC959 文件传输协议(FTP)翻译

发布时间:2023-07-12 21:14:54 前言 想做一下FTP的项目,带师说要参考RFC做才能标准化,先翻译一下。 官方文档:https://www.rfc-editor.org/rfc/inline-errata/rfc959.html 本备忘录的状态 本备忘录是文件传输协议&#xff08…

请求响应-数组集合参数的接受

数组集合参数 数组参数:请求参数名与形参数组名称相同且请求参数为多个,定义数组类型形参即可接受参数 具体关键代码如下: 在psotman中发出对应请求,结果如下: 集合参数:请求参数与形参集合名称相同且请求…

前端框架Layui实现动态树效果(书籍管理系统左侧下拉列表)

目录 一、前言 1.什么是树形菜单 2.树形菜单的使用场景 二、案例实现 1.需求分析 2.前期准备工作 ①导入依赖 ②工具类 BaseDao(通用增删改查) BuildTree(完成平级数据到父子级的转换) ResponseUtil(将数据转换成json格式进行回显&…

vue el-table的每行操作el-button添加单独的loading效果实现

vue el-table的每行操作el-button添加单独的loading效果实现 效果图&#xff1a;实现代码&#xff1a;结语 效果图&#xff1a; 实现代码&#xff1a; <tamplate><el-table :data"list" ><el-table-column fixed"right" label"操作&q…

java方法的可变参数

Java方法的参数列表可以包含可变参数。可变参数其实就是相同类型的多个参数构成的数组。 可变参数是类型后面跟着省略号&#xff08;…&#xff09;&#xff0c;然后空格&#xff0c;然后跟可变参数的名称。当然&#xff0c;类型和省略号之间可以包含空格&#xff0c;但不建议这…

【Hello mysql】 mysql的约束

Mysql专栏&#xff1a;Mysql 本篇博客简介&#xff1a;介绍mysql的约束 mysql的约束 表的约束空属性默认值列描述zerofill主键自增长唯一键外键总结 表的约束 为什么要有约束&#xff1f; 我们在收集一些数据的时候会要求该数据必须存在 比如说像是国家在登记公民信息的时候身…

ESP32开发板引脚介绍【附有引脚使用实例】

ESP32开发板引脚介绍 文章目录 ESP32开发板引脚介绍&#x1f468;‍&#x1f3eb;内容1&#xff1a;背景&#x1f468;‍⚖️内容2&#xff1a;限制类引脚&#x1f468;‍&#x1f4bb;内容3&#xff1a;ESP32 周边设备&#x1f349;文末备注 &#x1f468;‍&#x1f3eb; &am…

webpack的打包流程

webpack的打包流程 yarn build 开始都走了哪些流程 yarn build 开始都走了哪些流程