前言
在编程中,我们经常需要修改、处理数据。我们在对数据进行操作的时候,通常会先拷贝一份,这样我们才敢放心大胆的进行操作,JS 将拷贝分为了浅拷贝和深拷贝,有时候我们希望拷贝前后的数据相互不影响;有些时候,我们又希望二者相互影响。
本文将展开介绍这两种拷贝方式。
什么是浅拷贝与深拷贝?
浅拷贝 会创建一个新的对象,然后将原始对象中的每个属性的值复制到新对象中。如果属性的值是基本数据类型,直接复制其值;如果是引用类型,复制的是其引用地址,如果其中一个对象改变了这个地址,就会影响到另一个对象。
深拷贝 会拷贝所有的属性,并且如果这些属性是对象,也会对这些对象进行深拷贝,直到最底层的基本数据类型为止,所以无论你如何修改拷贝的数据,原始数据都保持不变。
通俗一点:
浅拷贝: 假设你有一个书架,书架上放着一些书。浅拷贝就好像你复制了一份书架的清单,但实际上并没有复制书架上的每一本书。如果你在清单上标记或添加了新的书,原来的书架也会受到影响。虽然你有两份清单,但实际上指向的是同一个书架。
深拷贝: 现在,想象一下你不仅复制了书架的清单,还把书架上的每一本书都复制了一份,放在一个全新的书架上。这样,无论你如何改变新书架上的书,原来的书架都不会受到影响。你拥有两个独立的书架,它们之间没有联系。
举个栗子:
js
let a = 1
let b = a // 拷贝一份 a,名为 b
a = 2 // 修改 a
console.log(b); //1 b不变
请问这是什么拷贝?
浅拷贝! 这里修改了原数据的值,副本没有改变,这不明显是深拷贝吗?
严格来说这里不能叫做副本,因为原始数据类型的值是简单的数据,拷贝时只需要复制它的值,而不需要复制其他的引用或结构。赋值操作 let b = a
发生的时候,实际上是将变量 b
关联到了变量 a
所引用的值1
上,而不是创建 a
的副本。所以我们修改 b
的值不会影响 a
。
所以我们不能通过判断数据是否相互影响来判断一个拷贝是浅拷贝还是深拷贝。
原始数据类型就是一个例外,它是浅拷贝,所以我们对拷贝的讨论一般不考虑原始数据类型,因为它只是简单的赋值操作,没有讨论的价值。
再来一个:
js
let obj = {
age: 18,
info: {
name: '阳阳',
city: 'New York'
}
};
let obj2 = obj //拷贝一份 obj
obj.age = 20 //修改 obj 的 age 属性值
obj.info.name = '羊羊' //修改 obj 的 info 属性的 name 属性值
console.log(obj2.age); //20 obj2属性值改变
console.log(obj2.info.name); //羊羊 obj2属性值改变
这里同样是一个浅拷贝 ,在这个例子中,obj2
和 obj
共享相同的引用地址,因此它们指向相同的对象。当你修改 obj
的属性时,实际上是修改了被引用的对象,因此 obj2
的相应属性也会受到影响。
对于引用类型的赋值,相当于是共享了引用地址,当我们修改了该地址的属性值,指向该地址的对象属性也会随之改变,这是浅拷贝的一大特征。
相比于浅拷贝,深拷贝则是对引用地址更深层次的探讨,如果这个属性是一个引用地址,则会继续对该地址存放的对象进行深拷贝,直到遍历到该属性为基本数据类型。
常见的浅拷贝操作
1. Object.create()
Object.create()
方法创建一个新对象,使用指定的对象原型或者 null。这种方式会创建一个新对象,但仅包含原对象的引用,因此是浅拷贝。
js
let a ={
name: '阳阳',
}
let b = Object.create(a)
console.log(b.name); //阳阳 在原型上拿到
2. Object.assign({}, x)
Object.assign()
方法用于将所有可枚举属性的值从一个或多个源对象复制到目标对象。同样,这也是一种浅拷贝方法。
js
let a = {
name : '阳阳',
like :{
first:'playing',
second:'coding'
}
}
let b = Object.assign({},a)
a.name = '羊羊'
a.like.first = 'coding'
console.log(b); //{ name: '阳阳', like: { first: 'coding', second: 'coding' } }
在这个例子中虽然,name
属性没有受到影响,但是嵌套的一个对象like
中的first
属性受到影响,所以还是浅拷贝。很容易理解,因为在a
对象中存的是like
这个嵌套对象的地址,因此,当你修改 a.like.first
时,实际上是修改了原始对象 a
中的 like
对象的属性。由于 b
中的 like
与 a
中的 like
共享相同的引用,所以 b.like.first
也会受到影响。
3. concat()
concat
方法用于合并两个或多个数组。当合并数组时,它返回一个新数组,包含原始数组的元素,也是一种浅拷贝。
js
let arr = [1,2,3,{a:10}]
let newArr = [].concat(arr)
arr.push(4)
console.log(newArr); // [ 1, 2, 3, { a: 10 } ] newArr不变
arr[0] = 10
console.log(newArr); // [ 1, 2, 3, { a: 10 } ] newArr不变
arr[3].a = 100
console.log(newArr); // [ 1, 2, 3, { a: 100 } ] newArr变
道理同上。
4. slice()
slice
方法返回一个数组的一部分,也是一种浅拷贝方法。
js
let arr = [1,2,3,{a:10}]
let newArr = arr.slice(0)
arr[3].a = 100
console.log(newArr); // [ 1, 2, 3, { a: 100 } ] newArr变
道理同上。
5. 数组解构
通过数组解构可以进行浅拷贝,但这仅适用于数组。
js
let arr = [1,2,3,{a:10}]
let newArr = [...arr]
arr[3].a = 100
console.log(newArr); // [ 1, 2, 3, { a: 100 } ] newArr变
道理同上。
6. arr.toReversed().reverse()
toReversed()
是新增的数组调转方法,它会返回一个调转后的新数组,不会影响原函数。我们先用toReversed()
调转数组并用newArr
接收这个数组,然后再用reverse()
调转回来,就能实现一个数组的浅拷贝。
js
let arr = [1,2,3,{a:10}]
let newArr = arr.toReversed().reverse()
arr[3].a = 100
console.log(newArr); // [ 1, 2, 3, { a: 100 } ] newArr变
这里还是存放引用地址这个问题,于是我们就可以根据这个规律手写一个浅拷贝。
手写一个浅拷贝
首先我们判断类型是否为引用类型,不是或者为空则直接返回,然后再判断引用类型是数组还是对象,接着我们用for in
遍历这个对象,它会遍历对象及其原型链上的可枚举属性,然而原型上的属性不是我们考虑的范围,所以我们用hasOwnProperty()
来判断属性是否为对象本身拥有,是则拷贝,不是则跳过,最终返回拷贝的后对象。
代码实现如下:
js
function shalldowCopy(obj) {
if(typeof obj !== 'object' || obj == null) return//只拷贝引用类型并且除去null
let objCopy = obj instanceof Array ? [] : {} //判断是数组还是对象
for (let key in obj) { //for in 会遍历到原型上的属性,所以要进行判断
if(obj.hasOwnProperty(key)){ //如果是对象自身的属性
objCopy[key] = obj[key] //则拷贝下来
}
}
return objCopy //返回拷贝后的对象
}
常见的深拷贝操作
JSON.parse(JSON.stringify(obj))
使用 JSON.stringify
将对象转换为 JSON 字符串,再使用 JSON.parse
将字符串解析为对象。这种方式是一种简单的深拷贝方法。
js
let obj = {
name:'阳阳',
age:18,
like:{
type:'coding'
}
}
let newObj2 = JSON.parse(JSON.stringify(obj)) //将obj转化为字符串,再转换为对象
obj.like,type = 'doing'
console.log(newObj2); //{ name: '阳阳', age: 18, like: { type: 'coding' } } 嵌套对象也没改变
手写一个深拷贝:
我们发现深拷贝其实就是循环嵌套拷贝对象中的每一个属性,如果拷贝到的这个属性是对象,也会对这些对象进行深拷贝,直到最底层的基本数据类型为止,这里就用到了递归思想。这意味着,对于深拷贝后的对象,即使原对象的属性值发生了变化,深拷贝后的对象的属性值也不会受到影响。
js
function deepCopy(obj) {
if (obj === null || typeof obj !== 'object') return
let objCopy = obj instanceof Array ? [] : {}
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
if (obj[key] instanceof Object) { //判断该属性是否为对象
objCopy[key] = deepCopy(obj[key]); // 是则继续深拷贝
} else {
objCopy[key] = obj[key]; // 不是则直接拷贝
}
}
}
return objCopy;
}
我们在浅拷贝的基础上,再判断完该属性是不是对象自己拥有的之后,再判断当前遍历到的属性是否是对象,如果是,则再对其深拷贝,反之则拷贝该属性。
关于面试
浅拷贝和深拷贝是在面试中往往是重点考查对象。这两个概念涉及到如何处理对象和数组的复制,对于避免引用共享和数据修改的副作用至关重要。如果你能理解浅拷贝和深拷贝,并手写它们的实现过程,那么在面试中,你又能跨过一座大山!
总结
浅拷贝:
- 复制对象的一层属性,不处理嵌套引用关系。
- 适用于简单对象和不需要处理嵌套引用的场景。
深拷贝:
- 完全复制对象及其所有嵌套属性,独立于原始对象。
- 适用于包含嵌套结构、引用类型、特殊对象类型等情况的对象或数组。
最后
已将学习代码放入Gitee中,欢迎大家学习指正!
我的Gitee: CodeSpace (gitee.com)
技术小白记录学习过程,有错误或不解的地方还请评论区留言,如果这篇文章对你有所帮助请 "点赞 收藏+关注" ,感谢支持!!