面试官:手写一个浅拷贝和一个深拷贝(拷贝详解)

两个拷贝都是JavaScript中应该必会的知识点,这两个东西基本上面试都会被问到,手写两个拷贝也理应是有手就会,如果你还不清楚拷贝是啥,或者不懂如何手写代码,不妨现在看下去

JavaScript中拷贝一般都是针对引用类型来说,先给大家看下深浅分别是什么样子的

ini 复制代码
let a = 1
let b = a
a = 2
console.log(b) // 1

这里a赋值到b后,b的值不会受a的变化而变化,传值而非址 ,这里其实也有底层逻辑在里面的,不妨看看我这篇预编译

像这样,一个数据拷贝另一个数据,原数据改变不会影响到另一个数据的拷贝,就是深拷贝,其实很好理解,就是拷贝得很彻底!不会再变了

ini 复制代码
let obj1 = {
	age: 18
}
let obj2 = obj1
obj1.age = 20
console.log(obj2.age) // 20

引用类型在调用栈中存放的是地址,传址而非值,引用类型真正存放在堆中,obj1赋值给obj2是地址,因此obj1和obj2共用一个地址,你改我也改

像这样,一个数据拷贝另一个数据,原数据改变会影响到另一个数据,就是浅拷贝,同理,就是拷贝得不彻底,后面还能变

既然拷贝都是针对引用类型来说,我们从这里就可以看出,普通对象的赋值是一个浅拷贝,那有没有其他手段,也能实现出浅拷贝,下面就开始介绍实现浅拷贝的方法

浅拷贝

下面两个是对象的浅拷贝常见手段

Object.create(x)

上次见这个方法还是创造对象的时候,这个方法创造的对象是一个空对象,并且会隐式继承原对象的属性

举个栗子🌰

css 复制代码
let a = {
	name: '小黑子'
}
let b = Object.create(a)
console.log(b.name) // 小黑子
console.log(b) // {} 

我们展开这个{}

现在看看这个方法的浅拷贝体现

css 复制代码
let a = {
	name: '小黑子'
}
let b = Object.create(a)
a.name = '大黑子'
console.log(b.name) // 大黑子

没有问题,你改我也改

Object.assign({}, x)

Object.assign是用来合并对象的,我们先认识下

举个栗子🌰

css 复制代码
let a = {
	name: '小黑子',
	hobby: {
		n: 'running'
	}
}
let b = {
	age: 18
}
let c = Object.assign(a, b)
console.log(c) // { name: '小黑子', hobby: { n: 'running' }, age: 18 }

我们现在来看下它的浅拷贝体现

css 复制代码
let a = {
	name: '小黑子',
	hobby: {
		n: 'running'
	}
}
let b = Object.assign({}, a)
a.name = '大黑子'
console.log(b.name) // 小黑子 --- WTF???

啊哈!笔者你逗我,说好的浅拷贝呢,别急,我们再来试试

css 复制代码
let a = {
	name: '小黑子',
	hobby: {
		n: 'running'
	}
}
let b = Object.assign({}, a)
a.hobby = {}
console.log(b.hobby) // running --- WTF?????? 

额......怎么回事,这两个栗子都是深拷贝啊!Hold on! 我们再来看一个

css 复制代码
let a = {
	name: '小黑子',
	hobby: {
		n: 'running'
	}
}
let b = Object.assign({}, a)
a.hobby.n = 'coding'
console.log(b.hobby) // coding

这里确实是浅拷贝,那这个方法怎么说,怎么既有浅拷贝又有深拷贝,我们不妨仔细分析下,a对象中有个hobby键,这个键的值又是一个对象,a.hobby.n = 'coding'而这个操作其实就是针对一个引用类型就行修改,一定会随之改变的,也就是说,assign是浅拷贝操作,如果对象的属性是引用类型,则就相当于拷贝了一个引用地址。当然,你也可以说深得不够彻底(前两个),还是浅!

数组也有浅拷贝的手段,下面四个是数组浅拷贝

concat

用于合并数组,返回一个新数组,不会修改原数组

浅拷贝体现🌰

ini 复制代码
let arr = [1, 2, 3, {a: 10}]
let newArr = [].concat(arr)
arr[3].a = 1
console.log(newArr) // [ 1, 2, 3, { a: 1 } ]

没有问题

slice

提取数组的一部分,返回一个新数组,不会修改原数组,参数一是起始下标,参数二是终止下标,左闭右开

浅拷贝体现🌰

ini 复制代码
let arr = [1, 2, 3, {a: 10}]
let newArr = arr.slice(0)
// 只有一个参数0,也就是提取整个数组
arr[3].a = 1
console.log(newArr) // [ 1, 2, 3, { a: 1 } ]

没有问题

打消你的疑惑:

既然都上slice了,我splice为何不可浅拷贝?

splice可以返回值,确实是可以获得一个新的数组,但是你要清楚,我们是来拷贝的,splice会影响原数组,拷贝完了你还把人家给删了,怕不是......

数组解构

[...arr]数组解构是es6新增的方法,解构是从数组中提取元素进行赋值

浅拷贝体现🌰

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

没有问题

arr.toReversed().reverse()

arr.toReversed()方法对应着reverse()都是用来颠倒数组,这个方法比较新,可能大家的node还不认识,可以去浏览器试试。阮一峰老师书中也有介绍这个方法,这里放个链接,请点击。因为reverse会影响原数组,并返回一个新数组,所以我们不用两个reversetoReversed()不影响原数组,所以这个方法刚好可以进行一个拷贝

浅拷贝体现🌰

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

没有问题

for

在手写之前,我们需要认识下for infor of此前文章没有详细讲过

for有三种遍历,一种是普通的for循环,还有两种分别为for infor of

for of

for of是天生给具有迭代器(Iterator)属性的数据结构遍历的,普通对象是没有的,数组有,因此你也可以拿他来遍历数组,今天我们主要是针对对象,所以这个方法我们不用

这个方法我此前文章有讲到过,贴个链接(这个文章没啥流量(悲)

Iterator-Set-Map-WeakSet-弱引用详解 - 掘金 (juejin.cn)

用例🌰

scss 复制代码
let arr = ['a', 'b', 'c', 'd', 'e']
for(let item of arr){
    console.log(item);
} // abcde

for in

for in是专门用来遍历对象的,既然能遍历对象也就能遍历数组。但是对今天的拷贝来说有个缺陷,就是for in甚至可以遍历到隐式具有的属性,我们拷贝不会去拷贝人家隐式的属性

请看下面的栗子🌰

javascript 复制代码
let obj = {
    name: '小黑子',
    age: 18,
    hobby: {
        type: 'coding'
    }
}
let newObj = Object.create(obj)
newObj.sex = 'boy'
for(let key in newObj){
    console.log(key);
} // sex name age hobby

如果我们想要手搓一个浅拷贝,必然是要遍历出对象的key的,但是我们不能要人家的隐式东西,解决这个问题,我们只需要判断一下,刚好有个属性就是判断隐式属性的

newObj.hasOwnProperty(key)

该方法返回布尔值,true代表显示,false代表不具有或者隐式

因此我们在for中添加一个判断就可以隔绝掉隐式属性

vbnet 复制代码
let obj = {
    name: '小黑子',
    age: 18,
    hobby: {
        type: 'coding'
    }
}
let newObj = Object.create(obj)
newObj.sex = 'boy'
for(let key in newObj){
    if(newObj.hasOwnProperty(key)){
        console.log(key);
    }
} // key

面试官:手搓一个浅拷贝

思路:既然是拷贝,必然是对一个对象进行拷贝,拷贝一般就是拷贝对象和数组,其余不考虑,因此这里必然先对形参判断一下。如果接收的形参是对象就给一个空对象,是数组就先给一个空数组,然后再进行遍历赋值,赋值必然是复制人家的key和value,人家的value如果还是一个引用类型,那刚好就是一个浅拷贝,因为赋的是地址

开始手搓

javascript 复制代码
function shalldowCopy(obj){
    // 只拷贝引用类型
    if(typeof obj !== 'object' || obj == null) return 

    let objCopy = obj instanceof Array ? [] : {}

    for(let key in obj){
        // 不要隐式
        if(obj.hasOwnProperty(key)){
            // objCopy.key是个字符串,[]可以当成变量
            objCopy[key] = obj[key]
        }
    }
    return objCopy
}

试试看🌰

ini 复制代码
let obj = {
    name: '小黑子',
    age: 18,
    hobby: {
        type: 'coding'
    }
}
let arr = ['a', {n: 1}, 1, undefined, null]
let newObj = shalldowCopy(obj)
let newArr = shalldowCopy(arr)
obj.age = 20
obj.hobby.type = 'swimming'
arr[0] = 'b'
arr[1].n = 2
console.log(newObj); // { name: '小黑子', age: 18, hobby: { type: 'swimming' } }
console.log(newArr); // [ 'a', { n: 2 }, 1, undefined, null ]

完美!

深拷贝

深拷贝只有一个自带的方法:JSON.parse(JSON.stringify(obj))

JSON.parse(JSON.stringify(obj))

JSON是前后端数据传输的一种数据交互格式,类似对象,它的key都是字符串

JSON.stringify(obj)

这个方法将对象转换成JSON字符串格式

举个栗子🌰

css 复制代码
let obj = {
    name: '小黑子',
    age: 18,
    hobby: {
        type: 'coding'
    },
    a: undefined,
    b: null,
    c: function() {},
    d: {
        n: 100
    },
    e: Symbol('hello')
}
console.log(JSON.stringify(obj)); // {"name":"小黑子","age":18,"hobby":{"type":"coding"},"b":null,"d":{"n":100}}

key都变成了字符串

JSON.parse(JSON.stringify(obj))

将JSON格式转换成对象

上面那个栗子🌰

yaml 复制代码
console.log(JSON.parse(JSON.stringify(obj))); 
// 输出如下
{
  name: '小黑子',
  age: 18,
  hobby: { type: 'coding' },
  b: null,
  d: { n: 100 }
}

大家发现没有,这个方法把人家的undefinedfunctionsymbol都吞掉了,起始还有一个bigint也无法展示,bigint会报错

浅拷贝体现🌰

javascript 复制代码
let obj = {
    name: '小黑子',
    age: 18,
    hobby: {
        type: 'coding'
    },
    b: null,
    d: {
        n: 100
    }
}
let newObj = JSON.parse(JSON.stringify(obj));
obj.hobby.type = 'running'
console.log(newObj.hobby.type); // coding

没有问题,是深拷贝

循环引用

对象之间存在相互引用的现象

举个栗子🌰

yaml 复制代码
let obj = {
    name: '小黑子',
    age: 18,
    hobby: {
        type: 'coding'
    },
    b: null,
    c: function() {},
    d: {
        n: 100
    }
}
obj.c = obj.d
obj.d.n = obj.c
console.log(obj)
// 输出如下
{
  name: '小黑子',
  age: 18,
  hobby: { type: 'coding' },
  b: null,
  c: <ref *1> { n: [Circular *1] }, // 循环
  d: <ref *1> { n: [Circular *1] }  // 循环
}

如果是带有循环引用的对象,我能否对其用JSON.parse(JSON.stringify(obj))进行深拷贝呢?

答案是不行的,会报错

因此这个方法有两个缺陷

  • 无法拷贝undefined,function,symbol,BigInt这几种数据类型
  • 无法处理循环引用

面试官:手搓一个深拷贝

实际上,深拷贝被面试问到的概率更大
思路:既然你对象中的引用类型不能随之修改,碰到这种我们直接再创建一个新的对象就可以了!这里用递归的思想会很巧妙,刚好递归的时候就创建了一个新的引用对象,其余和浅拷贝一样的,当然如果key是原始类型,就是直接赋值

开始手搓

scss 复制代码
function deepCopy(obj){
    let objCopy = {}
    for(let key in obj){
        if(obj.hasOwnProperty(key)){
                //obj[key]原始可以直接用,非原始就操作一下
            if(obj[key] instanceof Object){
                // 引用类型 递归就可以创建一个新的objCopy
                // 右边就是hobby这个对象
                objCopy[key] = deepCopy(obj[key])
            }else{
                // 原始类型
            objCopy[key] = obj[key] 
            }
            // 出口:if进不去就出来了
        }
    }
    return objCopy
}

试试看🌰

ini 复制代码
let obj = {
    name: '小黑子',
    age: 18,
    hobby: {
        type: 'coding'
    }
}
let newObj = deepCopy(obj)
obj.hobby.type = 'running'
console.log(newObj.hobby.type); // coding

完美!

如果你传的是数组,你就直接前面进行一个判断就可以,实际上,上面的手搓代码足以让面试官认可你了,如果非要刁钻!非得给你所有的数据类型,我们就用下面的方法,当然JSON.parse(JSON.stringify(obj))深拷贝的两个缺陷你也可以去引用下面两个库去解决

underscore

lodash

上面两个库都可以作为很好的资源去学习源码,像什么拷贝,去重,扁平化各种API都有封装,我们要做的是去实现他的源码


如果觉得本文对你有帮助的话,可以给个免费的赞吗[doge] 还有可以给我的gitee链接codeSpace: 记录coding中的点点滴滴 (gitee.com)点一个免费的star吗[星星眼]

相关推荐
腾讯TNTWeb前端团队3 小时前
helux v5 发布了,像pinia一样优雅地管理你的react状态吧
前端·javascript·react.js
uhakadotcom7 小时前
视频直播与视频点播:基础知识与应用场景
后端·面试·架构
范文杰7 小时前
AI 时代如何更高效开发前端组件?21st.dev 给了一种答案
前端·ai编程
拉不动的猪7 小时前
刷刷题50(常见的js数据通信与渲染问题)
前端·javascript·面试
拉不动的猪7 小时前
JS多线程Webworks中的几种实战场景演示
前端·javascript·面试
FreeCultureBoy8 小时前
macOS 命令行 原生挂载 webdav 方法
前端
uhakadotcom8 小时前
快速开始使用 n8n
后端·面试·github
uhakadotcom8 小时前
Astro 框架:快速构建内容驱动型网站的利器
前端·javascript·面试
uhakadotcom8 小时前
了解Nest.js和Next.js:如何选择合适的框架
前端·javascript·面试
uhakadotcom8 小时前
React与Next.js:基础知识及应用场景
前端·面试·github