当前位置: 首页 > news >正文

面试高频-深拷贝和浅拷贝

前言

深浅拷贝一直是前端里面的一个热门考点,不难,但是平常很实用,且面试方面也是经常被拿出来考查。

所以该说不说,深浅拷贝是一个合格的前端开发者必须掌握的一点。

正文

深浅拷贝介绍

什么是深拷贝?什么是浅拷贝?

深拷贝:深拷贝是对于对象的一个深度的复制,但是说复制其实不太准确,因为深拷贝从里到外都已经是一个新的对象了,与其说是复制,不如说是“照葫芦画瓢”,照着原本的对象创建出的一个新的对象,而新的对象对于原本的对象只不过是进行属性上的赋值,而不是复制,遇到基本类型就赋值,遇到引用数据类型就递归遍历到基本数据类型,再赋值,白话来说就是深拷贝出来的对象不管做任何修改都不会影响原对象

浅拷贝:浅拷贝是对于对象的一个“低层面”的一个复制,所谓低层面说白了就是该对象的第一层属性进行一次复制,不会进行更深层次的复制,如果遇到基本数据类型,就会进行赋值,而遇到引用类型则会进行复制。

那么出现深浅拷贝的原因是什么呢?

在 Javascript 里面,数据类型被分为两类,一类是基本数据类型(Number,String,Boolean,null,undefined,Symbol,Bigint),另一类则是引用类型(Object,Array,Function),而区别就是发生在这两种数据类型的区别上的。

基本数据类型因为占用内存较小,被直接存放在栈中,每次去使用基本数据类型变量的时候都等同于直接去栈空间里拿到这个值进行操作,而基本类型复制的时候,计算机会在栈内存里重新开辟出一个新的空间,用来存放新的基本数据类型,而基本数据类型的复制不应该叫复制,更应该叫“赋值”。

引用数据类型,因为大小没有一个固定的大小1,通常会很大,所以它被存放于堆空间内,而一般计算机不会直接去堆空间去取数据,所以引用数据类型在被存放在堆空间的同时,还会在栈内存空间去创建一个“引用地址”,即指针,它指向堆空间所存放的引用数据类型。

每次我们去拿取引用数据类型的变量的时候,都是先去拿存放在栈空间的引用地址,在通过引用地址的指向去到堆空间拿取引用数据变量;所以我们通常对引用类型进行各种行为其实都是在操作栈空间里的引用地址,复制也是同理,我们进行引用类型复制的时候其实就是对栈空间内的引用地址进行复制,最后还是指向同一个引用数据变量,所以就会出现下面这个情况。

js 代码解读复制代码let arr1 = [1,2,3];
let arr2 = arr1;
arr2[0] = 2;console.log(arr1);    //[2,2,3]
console.log(arr2);    //[2,2,3]

修改复制后的数组,原数组也会随之改变,引用两个引用类型变量所指向的都是同一个堆内存地址。

那么问题来了,如果我们想要去对引用数据类型进行一个类似于简单数据类型的复制怎么办?(即我操作复制后的变量,不会影响到原变量)

深浅拷贝原理

刚才说到怎么让一个引用数据类型的变量也能做到简单数据类型变量的那种复制对吧?其实很简单,让它遵循简单数据类型的原则不就可以了吗?我们可以把引用数据类型里面的每一个属性或者子元素拆分出来,一个个单独进行“赋值”,那么创造出来的新的变量就会是一个全新的变量,而不是与原变量共用一个堆空间。

比如数组的“复制”:

js 代码解读复制代码let arr1 = [1,2,3];
let arr2 = [];
for(let i=0;i<arr1.length;i++){arr2.push(arr1[i])
}
arr2[0] = 2;console.log(arr1);    //[1,2,3]
console.log(arr2);    //[2,2,3]

这段代码里,arr2 是我们新创建的一个数组,而里面每一个子元素都是通过对 arr1 数组里的子元素进行赋值添加的,而每个子元素都相当于是一个基本数据类型。

那么如果遇到对象呢?其实也和数组是是一样的方式

js 代码解读复制代码let obj1 = {name:'李华',sex:'男',age:'18'};
let obj2 = {};
for(let i in obj1){obj2[i] = obj1[i];
}
obj2.name = '橙心';console.log(obj1);    //{name: '李华', sex: '男', age: '18'}
console.log(obj2);    //{name: '橙心', sex: '男', age: '18'}

而这就是深浅拷贝的基本原理

浅拷贝

浅拷贝上面也是说了,它只对引用数据类型进行一层次的拷贝,即不管数组是几维数组,对象有多少层属性,浅拷贝都只会对它的第一层属性进行复制。

而下面这就是浅拷贝的实现:

js 代码解读复制代码// 浅拷贝函数
function shallowCopy(obj) {// 判断参数是否为对象类型,如果不是则直接返回if (typeof obj !== "object" || obj === null) return obj;if(Array.isArray(obj)){   //判断是否是数组let newObj = [];for(let item in obj){newObj.push(item);}return newObj;}else{         //若是对象// 创建一个新对象,用于存储拷贝后的属性值let newObj = {};// 遍历原对象的属性,并将其复制到新对象上for (let key in obj) {// 判断是否为自身属性(非原型链上继承来的)if (obj.hasOwnProperty(key)) {// 如果是,则直接赋值即可(注意这里可能会拷贝引用地址)newObj[key] = obj[key];}}// 返回新对象return newObj;}
}

咱们再来用一点例子测试测试:

js 代码解读复制代码let obj1 = {name:'李华',fimaly:{father:'李青'}}
let obj2 = shallowCopy(obj1);
obj2.name = '刘然';
obj2.fimaly.father = '刘琦';
console.log(obj1);
console.log(obj2);

image-20240819204141641

然后你会发现,我们改变复制后的对象的第一层的 name 属性,原来的 obj1 并没有因此而改动,但是我们改fimaly 属性下的 father 属性却同时让原来的 obj1 身上的 father 属性同时也被修改了,这就是浅拷贝,仅仅拷贝第一层次。

除此之外,js能做到浅拷贝的内置方法还有:Object.assign(),扩展运算符“…”,Array.prototype.concat(),Array.prototype.slice()。

深拷贝

深拷贝,顾名思义,深层次的复制;文章上面有说拷贝的原理,但是上面写的那些例子不过是一层次的例子,真正要做到深拷贝,需要对这个引用类型由里到外的进行一次“复制”。

那么,如何做到深层次的拷贝呢?

答案是:递归!

递归是我们常用来处理深层次对象,二叉树,或者排列拼接常用的手段,那么深层次的拷贝,自然少不了递归调用,对每一层对象进行遍历;

以下为深拷贝递归算法实现:

js 代码解读复制代码function deepCopy(obj){let newObj = null;     // 创建一个用于存储拷贝赋值的新对象if(typeof obj != 'object' || obj === null){   //判断如果是基本数据类型直接赋值newObj = obj;}else{if(Array.isArray(obj)){   //判断是数组还是对象newObj = [];        //当为数组时给新对象进行赋值为数组for(let item in obj){      //遍历原数组属性,给新数组进行增加newObj.push(deepCopy(obj[item]))}}else{           //当为对象时newObj = {};        //当为对象时给新对象进行赋值为对象for(let item in obj){       // 遍历对象属性,给新对象的属性与原对象属性进行赋值newObj[item] = deepCopy(obj[item])}}}return newObj
}

这里咱们再用刚才那个例子吧

let obj1 = {name:'李华',fimaly:{father:'李青'}}
let obj2 = deepCopy(obj1);
obj2.name = '刘然';
obj2.fimaly.father = '刘琦';
console.log(obj1);
console.log(obj2);

image-20240819204322782

这下是不是对了?不论是深层次还是第一层次都是做到了改变新数组而原数组不会进行任何改变,这就是深拷贝。而 JS 能做到深拷贝的内置 API 也是有 JSON.parse(JSON.stringify(obj)) 的。

说到这里就不得不提一下lodash这个库了,lodash提供了cloneDeep()方法,允许我们对引用类型的值进行深拷贝,并且不会出现上面的情况。

const dc = require('lodash');
let obj = { name: 'Alice', age: 20 };
let newObj = dc.cloneDeep(obj);

不过据说还有另一种方法 ,但可惜目前好像除了谷歌浏览器以外没有支持的浏览器的,属于是剑走偏锋出奇效的了。本文不做讲解,感兴趣的各位可以自行了解

深拷贝和浅拷贝的应用场景

浅拷贝适用于只需要复制一层对象引用的情况,比如克隆一个对象用于修改而不影响原始对象。

深拷贝适用于需要完全独立的对象副本的情况,比如避免修改原始对象或者将对象传递给其他函数时产生副作用。

需要根据具体的业务需求选择合适的拷贝方式,避免出现意料之外的问题。

总结

深拷贝和浅拷贝是前端开发中常见的概念,对于理解和应用它们的区别非常重要。浅拷贝只复制对象的一层引用,而深拷贝递归地复制对象及其子对象。在实际开发中,我们要根据具体的需求选择合适的拷贝方式,以确保代码的正确性和效率。


http://www.mrgr.cn/news/3485.html

相关文章:

  • C/C++控制台贪吃蛇游戏的实现
  • esbuild中的Base64 Loader:轻松将文件编码为Base64字符串并嵌入代码
  • linux(arm)移植 macchanger
  • 有了这4款工具,你就知道电脑怎么录屏了!
  • CTRL-C论文解析
  • JS(三)——更改html内数据
  • MySQL 重复数据操作
  • 微服务多个模块启动,端口被占用,yml配置文件读不到
  • 【48 Pandas+Pyecharts | 2024年巴黎奥运会奖牌数据分析可视化】
  • 废品回收小程序,开启上门回收模式
  • 【ARM Hypervisor And SMMU 系列 5 -- SMMU 和 IOMMU技术】
  • 计算机基础之Cache的缓存命中率不随其容量线性增加的原理
  • nginx主配置文件说明
  • win10 / win11 永久暂停自动更新方法
  • LLM小模型系列研究(01)
  • STM32- 笔记2
  • CSS:display和visiblity
  • 小白学大模型:GLM 调用教程
  • FlinkCDC初体验
  • 关于c++ grpc 和 c# grpc 通信的问题 以及 grpc 认证问题