「这是我参与11月更文挑战的第3天,活动详情查看:2021最后一次更文挑战」
在日常开发过程中,经常会遇到需要拷贝数据的场景。要理解深浅拷贝,就需要从数据类型开始说起。JS的数据类型分为基本数据类型
和引用数据类型
,其中,基本数据类型的值存储在栈中,而引用数据类型的值存在堆中,栈只存储指向该堆存储位置的位置值。关于8种数据类型可以看我这篇文章 小陈同学の前端笔记 | 关于8种数据类型的那些事儿你还记得吗?。
赋值
赋值是将某个数值或者对象赋给某个变量的过程。
1.对基本数据类型进行操作,两个变量不会互相影响。
// 赋值
let a = '小陈同学吗'
let b = a
console.log(b)
a = '是小陆同学'
console.log(b)
复制代码
打印结果
2.对引用数据类型进行操作,两个变量会产生影响。
// 赋值
let boy = {
name: '小陈同学吗',
others: {
age:23,
interests: {
languages: ['JavaScript','Python'],
sports: ['乒乓球','羽毛球']
}
}
}
let girl = boy
console.log(girl)
// 修改对象内的基本数据类型
boy.name = '是小陆同学'
console.log(girl)
// 修改对象内的对象
boy.others.age = 100
console.log(girl.others.age)
复制代码
打印结果
总结:对于赋值操作,如果赋的是基本数据类型,则不会有影响;若是引用数据类型,则会随之变化而变化。
浅拷贝
浅拷贝是创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。它只拷贝第一层属性
如果拷贝的是基本数据类型,则不会有变化;若拷贝引用数据类型,则还是指向同一块地址,仍会产生影响。
下面来看看浅拷贝的一些实现方式。
Object.assign()
根据 MDN 的定义,Object.assign()
方法用于将所有可枚举属性的值从一个或多个源对象分配到目标对象。它将返回目标对象。
// 浅拷贝
let boy = {
name: '小陈同学吗',
others: {
age:23,
interests: {
languages: ['JavaScript','Python'],
sports: ['乒乓球','羽毛球']
}
}
}
let girl = Object.assign({},boy)
console.log(gril)
boy.name = '是小陆同学'
console.log(girl)
boy.others.age = 100
console.log(girl.others.age)
复制代码
打印结果
我们修改了boy
对象的name
,经浅拷贝的girl
对象的name
并没有被修改。而对象中的others
对象收到了影响,因为浅拷贝只拷贝第一层属性。
... 展开语法
let girl = {...boy}
console.log(girl)
boy.name = '是小陆同学'
console.log(girl)
boy.others.age = 100
console.log(girl.others.age)
复制代码
得到的是相同的结果。
手写的浅拷贝
function shallowCopy(obj) {
const resObj = {}
for(let item in obj){
if(obj.hasOwnProperty(item)){
resObj[item] = obj[item]
}
}
return resObj
}
// 调用浅拷贝函数
let girl = shallowCopy(boy)
复制代码
其他
除此之外,还有对数组进行浅拷贝的方法,如 Array.prototype.slice()
和Array.prototype.concat()
。执行的效果也是一样的。
深拷贝
因为浅拷贝只对第一层属性进行了拷贝,虽然保证了基本数据类型的改变不会受到影响,但对于引用数据类型,拷贝的只是对地址的引用,因此并不能完全脱离原对象的影响。因此,我们需要实现深拷贝,下面介绍几种深拷贝的实现方式。
JSON.parse(JSON.stringfy())
先用JSON.stringfy()
将原对象序列化成JSON字符串,之后再通过JSON.parse()
还原成一个新对象。
let boy = {
name: '小陈同学吗',
others: {
age:23,
interests: {
languages: ['JavaScript','Python'],
sports: ['乒乓球','羽毛球']
}
}
}
// 使用JSON.parse(JSON.stringfy())
let girl = JSON.parse(JSON.stringify(boy))
console.log(girl)
boy.name = '是小陆同学'
console.log(girl)
boy.others.age = 100
console.log(girl.others.age)
复制代码
打印结果
可以发现,经过深拷贝之后,改变boy
对象中的子对象others
也不会对拷贝后的girl
对象产生影响。
看上去一行代码就解决了问题,很好,对不对?
但这种方法存在很大的 局限性:
- 如果原对象中有
undefined、Symbol、function
时,会导致该键值被丢失- 如果原对象中有
正则
,会被转换为空对象{}
- 如果原对象中有
Date
,会被转换成字符串- 如果对象中存在
循环引用
的情况,会报错
我们来验证下第1、2、3点局限。
let testObj = {
age: 20,
name: undefined,
address: Symbol('123'),
func: function(){}
}
console.log(testObj)
let copyObj = JSON.parse(JSON.stringify(testObj))
console.log(copyObj)
复制代码
打印结果
我们可以发现,经过这种方法深拷贝过后的对象里,直接忽视掉原对象中值为undefined
、Symbol
和function
的属性,正则
会被转换为空对象{}
,且Date
对象也会转换为字符串,并没有将真实的对象拷贝过来。
递归的浅拷贝
浅拷贝只复制第一层,遇到对象也只是复制对象的引用,那其实完全也可以进入这个子对象,对子对象的属性值再次进行复制,如若再遇到子子对象,则继续进入并复制……
听起来就是一个递归的过程,因此我们将用递归的形式来实现深拷贝。
function deepCopy(obj) {
const copyObj = {}
// 考虑特殊情况
if(obj == null) return obj
if(obj instanceof Date) return new Date(obj)
if(obj instanceof RegExp) return new RegExp(obj)
if(typeof obj !== 'object') return obj
for(let item in obj){
if(obj.hasOwnProperty(item))
copyObj[item] = deepCopy(obj[item])
}
return copyObj
}
let boy = {
name: '小陈同学吗',
others: {
age:23,
interests: {
languages: ['JavaScript','Python'],
sports: ['乒乓球','羽毛球']
}
},
xx: undefined,
address: Symbol('123'),
func: function(){},
reg: /[1-9]/,
time: new Date()
}
console.log(boy)
let girl = deepCopy(boy)
boy.name = '是小陆同学'
boy.others.age = 100
console.log(girl)
复制代码
打印结果
已经完完全全将boy
拷贝到girl
了,且修改boy
对象也不会对girl
产生影响,且上述的三个局限点在这个方法中也解决了。
但是我发现一个问题,代码中我明明是先打印的boy,然后将他的age修改为100,为什么打印出来的boy的age直接显示100了?
复制代码
结语
以上就是本期整理的有关浅拷贝与深拷贝的内容,写的不咋样也并没有把所有的情况都考虑到位了。
如有纰漏,欢迎指出!