两个拷贝都是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会影响原数组,并返回一个新数组,所以我们不用两个reverse
,toReversed()
不影响原数组,所以这个方法刚好可以进行一个拷贝
浅拷贝体现🌰
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 in
和for of
此前文章没有详细讲过
for有三种遍历,一种是普通的for循环,还有两种分别为for in
和for 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 }
}
大家发现没有,这个方法把人家的undefined
,function
,symbol
都吞掉了,起始还有一个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))
深拷贝的两个缺陷你也可以去引用下面两个库去解决
上面两个库都可以作为很好的资源去学习源码,像什么拷贝,去重,扁平化各种API都有封装,我们要做的是去实现他的源码
如果觉得本文对你有帮助的话,可以给个免费的赞吗[doge] 还有可以给我的gitee链接codeSpace: 记录coding中的点点滴滴 (gitee.com)点一个免费的star吗[星星眼]