[干货]面试手写浅拷贝和深拷贝?今天我们就来学一下!

前言

说到拷贝大家会想到啥?复制粘贴?没错,就是简单的复制粘贴!

那你知道JavaScript的拷贝有多"讲究"嘛?你知道什么是深拷贝和浅拷贝嘛?

如果面试官让你手写浅拷贝和深拷贝的方法怎么办?

今天,就带大家来了解一下JavaScript中的浅拷贝和深拷贝!

收藏===学会!大家可以手动学会一下哦~🥺

正文

说到拷贝,大家一定一定很很熟悉吧!不就是ctrl + cctrl + v嘛?

我们今天来讲一点不一样的!

在JavaScript中,对象的复制是一项常见的操作。然而,由于JavaScript中的对象是引用类型,开发者必须小心处理对象的复制,以避免在程序中出现意外的副作用。为了实现对象的复制,我们通常使用浅拷贝和深拷贝两种不同的技术。它们分别指的是对对象进行复制时,是否仅仅复制了对象的引用,还是创建了一个全新的对象。

什么是浅拷贝?

浅拷贝就是指当我们在复制对象时,仅仅是复制了对象的表面结构,而没有复制对象内部的嵌套对象!换句话说,浅拷贝创建的新对象,无时不刻不在受到原对象的影响,也就是说这个新对象的某些属性仍然是原对象的引用!

我们来看个案例:

js 复制代码
let obj ={
    age:18
}
//可以说obj2浅拷贝obj
let obj2 = obj
obj.age = 20
console.log(obj2.age);//输出:20

在这个案例当中,我们可以知道,对于对象这种引用类型,我们通过变量间的赋值,其实是一种浅拷贝的方式,为什么这么说呢?

我们都知道,对象是引用类型,也就是说,我们的objobj2两个变量其实存的是堆当中数据的地址!我们通过赋值操作,其实就是把obj的地址赋给obj2,我们通过obj去修改这个地址所在的位置中存储的属性,也就导致obj2读取这个地址中的属性也是被修改后的值,这样的一种拷贝方式其实就是浅拷贝

浅拷贝是一种对象复制的操作,它创建了一个新对象,并将原始对象的属性值复制到新对象中。然而,与深拷贝不同,浅拷贝只复制基本数据类型的属性值,而对于对象类型的属性,它们仍然是原始对象中相同对象的引用。换句话说,浅拷贝创建了一个新对象,但对于原始对象中的对象属性,新对象中的属性仍然指向原始对象中相应的属性。

我们再来看看浅拷贝的常见案例:

js 复制代码
// //常见的浅拷贝
let a = {name:'小黑'}
let b = Object.create(a)//让b对象继承于a
a.name = '小白'
console.log(b.name) 

这个案例,我们利用Object对象自带的方法create让对象b继承对象a,可以看到,我们通过修改a对象的属性name,再通过b对象去读取name属性,这个属性也被修改了,也就是说对象a和对象b其实指向的是同一个属性!

那我们想个办法!怎么样才能让对象实现不是浅拷贝呢? 大家会不会想到开辟一段新的地址?

让我们来试试!我们利用Object自带的方法assign()来将两个对象合并成一个对象,也就是开辟了一个新的引用地址,存储新的对象。

js 复制代码
//浅拷贝
let a = {name:'小黑', like:{
    n:'coding'
}}
//Object.assign()将两个对象合并成一个对象
let b = Object.assign({},a)
a.name = '小白'
// b.name不受影响
a.like.n = 'running'
// like也是一个对象,存的也是一个引用地址
console.log(b);//输出:{ name: '小黑', like: { n: 'running' } }

可以看到,我们通过这样设定的新对象b,是一个新的引用地址中存储的对象!

我们通过修改原对象aname属性,发现b对象中的属性并不受影响!

但是,我们若是修改b对象中的嵌套对象中的属性,则会影响b对象中的嵌套的like对象的属性!

这是因为,再b对象当中like对象存储的也是引用地址,在对象a中,存储的是引用地址

所以,在b对象当中,存储的也是引用地址!因此,a对象和b对象中的like对象指向的是同一个地址,仍然是浅拷贝!

我们再看看数组的浅拷贝!

js 复制代码
//浅拷贝
let arr = [1,2,3,{a:10}]
let newArr = [].concat(arr)//concat会返回一个新的数组
arr[3].a=100
console.log(newArr);//输出:[ 1, 2, 3, { a: 100 } ]

在我们这个案例当中,我们定义了一个arr数组,利用数组方法concat生成一个新的数组newArr,也就是引用一段新的地址,开辟一段新的空间存储新的数组!

但是,newArr中有引用的数据,比如{a:10}还是存储的是引用对象的地址!所以,我们还是能通过arr修改这个对象的属性a来影响我们新生成的数组newArr

同理,我们通过其他方法生成的新数组也是一样的效果!

例如splice方法

js 复制代码
let arr = [1,2,3,{a:10}]
//splice影响原数组  slice不影响原数组
let newArr = arr.slice(0)
arr[3].a = 100
console.log(newArr);//输出:[ 1, 2, 3, { a: 100 } ]

解构方法:

js 复制代码
let arr = [ 1,2,3,{a:10}]
let newArr = [...arr]
arr[3].a = 100
console.log(newArr);//输出:[ 1, 2, 3, { a: 100 } ]

浅拷贝的实现原理

现在,我们来介绍一下浅拷贝的实现原理!

我们直接先给大家展示代码吧!

js 复制代码
//浅拷贝的实现原理
let obj = {
    name:'小黑',
    age:18,
    like:{
        type:'coding'
    }
}
let arr = ['a',{n:1},1,undefined,null]

function shalldowCopy(obj){
    if(typeof obj !== 'object' ||obj ==null) return //只拷贝引用类型!
    let objcopy = obj instanceof Array ? []:{}

    for(let key in obj)
    {
        if(obj.hasOwnProperty(key))
        {
            //不能写.key 写.会直接变为字符串,我们要读的是变量
            objcopy[key] = obj[key]
        }
    }
    return objcopy
}
let newObj = shalldowCopy(obj);
obj.like.type = 'swimming'//key为like,读到的就是引用地址!
console.log(newObj);//输出:{ name: '小黑', age: 18, like: { type: 'swimming' } }

当然,这个代码还是一个盖板的代码,还有很多种条件,大家可以自己去优化一下!

我们来为大家介绍一下原理!

  1. 我们定义了一个要进行浅拷贝操作的对象obj和数组arr

  2. 我们又定义了一个shalldowCopy方法,我们通过这个方法来实现浅拷贝的原理!

  3. 在方法体当中,我们首先通过判断obj是否是引用类型!我们只需要拷贝引用类型~

  4. 再定义一个变量objcopy通过一个三元运算符来通过obj的类型来给这个变量赋值为数组或者对象!

  5. 我们再通过for...in循环遍历obj中的元素或者属性!

    为什么我们这里不用for ...of遍历呢?这是因为

    for...of:用于遍历可迭代对象(iterable objects),例如数组、字符串、Map、Set等。它遍历的是对象的值。

    for...in:用于遍历对象的可枚举属性,包括继承的属性。它遍历的是对象的键。

    对象不具有迭代属性,所以我们选择使用for ...in方法

  6. 通过hasOwnPropery(key)方法判断对象中是否具有这个属性,然后把这个obj的属性赋值给objcopy

    hasOwnProperty() 是 JavaScript 中 Object 对象的一个方法。该方法用于检查一个对象是否包含指定的属性(键),并返回一个布尔值,指示属性是否为对象的直接属性而非继承的属性。

  7. 最后返回objcopy,就实现了一个浅拷贝的原理!

什么是深拷贝?

深拷贝是指创建一个新对象,同时递归地复制原始对象及其所有嵌套的对象,确保新对象和原对象的每个属性都是相互独立的,没有共享引用。它不仅复制对象本身,还会递归地复制对象内部的所有嵌套对象。

例如:

js 复制代码
let a = 1
let b = a
a = 2
console.log(b);
//b是原始数据类型,存在调用栈当中,会直接取到a的值存到b中

对于原始数据的赋值就是一种深拷贝!我们修改a的值不会影响到b的值!

在JavaScript中,对象赋值默认是浅拷贝,这意味着当我们将一个对象赋值给另一个变量时,实际上是传递了对象的引用,而不是对象的副本。这样的赋值会导致两个变量指向相同的内存地址,修改其中一个变量会影响到另一个变量。深拷贝的作用在于创建对象的完全独立副本,避免对象之间的关联。

深拷贝的实现

JSON.stringifyJSON.parse

利用JSON.stringifyJSON.parse是一种简单的深拷贝方法。这种方法的优势是简单易懂,但它有一些限制,比如不能处理包含函数、循环引用等情况的对象。

我们看看这个案例:

js 复制代码
let obj = {
    name:'小黑',
    age:18,
    like:{
        type:'coding'
    }
}
//深拷贝的方法
//JSON.stringify(obj)把一个对象变为字符串, JSON.parse把一个字符串变为对象
console.log(JSON.stringify(obj));//输出:{"name":"小黑","age":18,"like":{"type":"coding"}}
console.log(JSON.parse(JSON.stringify(obj)));//输出:{ name: '小黑', age: 18, like: { type: 'coding' } }
let newObj = JSON.parse(JSON.stringify(obj))
obj.like.type = 'eating'
console.log(newObj);//输出:{ name: '小黑', age: 18, like: { type: 'coding' } }

可以看到,我们通过利用JSON.stringifyJSON.parse就可以简单地实现一个深拷贝的方法!这是官方提供的方法!

但是这个方法存在诸多限制!这个方法拷贝不了函数体,拷贝不了undefined,拷贝不了BigInt,拷贝不了Symbol,BigInt都处理不了!

当然,我们也可以自己手写一个递归方法实现深拷贝的方法!

手写深拷贝

js 复制代码
// 面试题 手动实现一个深拷贝的方法
let obj = {
    name:'小黑',
    age:18,
    like:{
        type:'coding'
    }
}
var copyobj = deepCopy(obj)
obj.type = 'running'
console.log(copyobj);//输出:{ name: '小黑', age: 18, like: { type: 'coding' } }
//换一个地址
//这个如果对象存在循环引用就会爆栈
function deepCopy(obj) {
    let objCopy = {}
    for(let key in obj){
        if(obj.hasOwnProperty(key)){
            if(obj[key] instanceof Object)//obj[key] i是引用类型
            {
                objCopy[key] = deepCopy(obj[key])
            }else
            {
                objCopy[key] = obj[key]
            }
        }
    }
    return objCopy
}

//Jquery库,这个库很经典
//https://www.underscorejs.cn/  封装了好多方法

当然,这也是一个面试题哦~

我们接下来为大家来分析一下这个是如何实现的!

  1. 同样的,我定义了一个obj对象,这是一个嵌套的对象,其中嵌套了一个like对象!
  2. 我们定义了一个函数deepCopy函数,用于实现我们深拷贝的逻辑!
  3. 首先,我们定义了一个objCopy为空对象
  4. 通过for ...in方法,对传过来的对象进行遍历!
  5. 然后,我们通过一个hasOwnProperty()方法,获取对象当中的key值,键名,
  6. 判断这个key值所存储的值是否是引用类型
  7. 如果是,则进行函数自调用,在同样的逻辑下去拷贝对象体内的引用类型。
  8. 如果是原始类型,则直接复制就好了!

这样我们就实现了一个深拷贝的方法!主要的思想是什么呢?

就是在我们的方法当中,每次我们都开辟一个新的对象,使用一个新的地址,然后去遍历我们传过来的对象,如果是引用类型,我们就再调用我们自己的方法,用同一个逻辑,每次有引用类型,都去开辟新的引用地址,用新的内存去存储,直到对象当中没有引用类型!

注意!!我们手写的方法应对不了循环引用(这个我们再这里就不多阐述)

如果是原始类型,我们可以直接进行拷贝,因为原始类型的拷贝都是值拷贝,其实差不多就是深拷贝!、

这样我们深拷贝的逻辑就实现了!

其实还有一种方法就是大家可以导入Jquery库,这个库中封装了很多方法!大家感兴趣可以自行去了解一下哦!

总结

拷贝

通常只针对于引用类型!针对原始类型毫无意义

a = b 谈不上拷贝!

浅拷贝

新的对象受老的对象的影响

  • 常见的浅拷贝方法:
  1. object.create(x)
  2. object.assign({},x)
  3. concat
  4. slice
  5. 数组解构
  6. arr.toReversed().reverse()

深拷贝

有一个偏门方法可以处理

  • 常见的深拷贝方法: JSON.parse(JSON.stringify(obj))

  • 缺点

  1. 无法拷贝undefined,function,Symbol,BigInt这几种类型的数据
  2. 无法处理循环引用!

引用类型的赋值是浅拷贝,原始类型是深拷贝,聊拷贝忽视原始类型 拷贝只针对引用类型

好了!我们有关JavaScript中的浅拷贝和深拷贝就学习到这里啦!

大家有任何想法和意见欢迎评论留言!点个赞鼓励支持一下吧!🌹🌹🌹🤩

个人gitee库:MycodeSpace: 主要应用的仓库,记录学习coding中的点点滴滴 (gitee.com)

相关推荐
Python私教25 分钟前
Vue3中的`ref`与`reactive`:定义、区别、适用场景及总结
前端·javascript·vue.js
CQU_JIAKE27 分钟前
12.12【java exp4】react table全局搜索tailwindcss 布局 (Layout) css美化 3. (rowId: number
前端·javascript·react.js
m0_748236581 小时前
Django 后端数据传给前端
前端·数据库·django
余生H1 小时前
前端Python应用指南(五)用FastAPI快速构建高性能API
前端·python·fastapi
racerun1 小时前
Vue vuex.store mapState
前端·javascript·vue.js
2301_801074151 小时前
ArkTs组件(2)
开发语言·前端·华为·harmonyos
yep吖2 小时前
Datawhale-AI冬令营二期
开发语言·javascript·ecmascript
DT——2 小时前
Sass复习篇
前端·css·sass
胡西风_foxww2 小时前
【ES6复习笔记】箭头函数(5)
javascript·笔记·es6·函数·箭头·箭头函数
m0_748251722 小时前
ollama-webui - Ollama的ChatGPT 风格的 Web 界面
前端·chatgpt