了解js中的“复印机”:深浅拷贝
前言
在JavaScript中,拷贝(copy) 是指创建一个数据结构的副本,这个副本可以是一个变量、数组或对象等。根据拷贝的深度,可以分为浅拷贝(shallow copy)和深拷贝(deep copy)。简而言之就是将对象再复制一份,但是,复制的方法不同将会得到不同的结果。
数据存储
在了解深浅拷贝前我们得先了解一下两种数据类型是怎么存储的。首先是基本数据类型(如数字、字符串、布尔等),它们都是直接存储在栈内存中,每个变量都包含实际的值。这意味着,当一个基本类型的变量被赋予另一个变量时,会创建该值的一个副本,两个变量分别独立存储各自的值。然后就是引用数据类型(如数组、对象、函数等)、他们都是存储在堆内存中,而变量中存储的是指向堆中对象的引用(内存地址)。当一个引用类型的变量被赋予另一个变量时,实际上是复制了引用,两个变量最终指向的是同一块堆内存中的对象,因此修改其中一个变量会影响到另一个。例如:
let a = 2
let obj={
b:1
}
上面代码我们通过下图来观察一下他们的存储位置都在哪:
上述代码通过预编译a
放入全局的变量环境赋值为2,obj
也在全局的变量环境里但他的赋值相当于是堆里的一个编号,通过这个编号才能从堆里访问到obj
里的b:1
。通过上述介绍深浅拷贝这个我们基本不考虑基本数据类型,因为基本数据类型没有引用地址一说。
浅拷贝
介绍完数据是如何存储的了,现在回到我们的主题:首先来了解一下浅拷贝。浅拷贝是创建一个新对象,该对象与原始对象共享相同的内存地址,因此对新对象的修改也会影响原始对象。浅拷贝通常只复制对象的基本属性,而不复制对象的引用类型属性。通俗讲就是拷贝后的对象受原对象的影响。
常用的浅拷贝方法
1. Object.create(obj)
Object.create(obj)
的作用是创建一个新的对象 obj2
,并将其原型 (__proto__
) 设置为 obj
。这意味着 obj2
会通过原型链查找访问 obj
上的属性和方法。
let obj = {
a:1
}
let obj2 = Object.create(obj)
console.log(obj2.a);
obj.a=2
console.log(obj2.a);
2. Object.assign({}, obj)
Object.assign({}, obj)
用来创建一个新的obj2
的空对象,再将obj
的所有可枚举属性复制给obj2
;也可以用Object.assign({}, obj)
将b
中的属性复制给a
,相当于合并。
let obj = {
a:1
}
let obj2 = Object.assign({}, obj)
obj.a=2
console.log(obj2.a);
console.log(obj2);
let a={n:1}
let b={m:2}
console.log(Object.assign(a,b));
3. [].concat(arr)
[].concat(arr)
用于合并数组,创建一个空数组[]
,然后将arr
的属性复制给空数组,从而创建一个新数组arr2
。
let arr=[1,2,3,{a:1}]
let arr2=[].concat(arr)
arr.push(4)
arr[3].a=2
console.log(arr2);
console.log(arr2[3]);
4. 数组解构[...arr]
[...arr]
在此代码中的作用是创建了一个空数组arr2
,然后将arr
的原始复制给arr2
let arr=[1,2,3,{a:1}]
let arr2=[...arr]
arr.push(4)
arr[3].a=2
console.log(arr2);
console.log(arr2[3]);
5.arr.slice(0)
创建一个arr2
空数组,然后通过arr.slice(0)
将数组arr
的全部元素切下来复制给数组arr2
let arr=[1,2,3,{a:1}]
let arr2=arr.slice(0)
arr.push(4)
arr[3].a=2
console.log(arr2);
console.log(arr2[3]);
6.arr.toReversed().reverse()
通过toReversed()
反转且生成一个新数组,再通过reverse()
将生成的新数组再反转,得到与arr
一样的数组arr2
。
let arr=[1,2,3,{a:1}]
let arr2=arr.toReversed().reverse()
arr.push(4)
arr[3].a=2
console.log(arr2);
console.log(arr2[3]);
手动实现浅拷贝
讲完上述六种方法,突然面试官来一句让你自己手动实现浅拷贝的方法,别慌,看完下面代码就有办法了
function shallowCopy(obj){
let newObj={}
for(let key in obj){
// 判断key 是不是obj显示具有的
if(obj.hasOwnProperty(key)){
newObj[key]=obj[key]
}
}
return newObj;
}
Object.prototype.c=3
let obj ={
a:1,
b:{n:2}
}
console.log(shallowCopy(obj));
console.log(obj.c);
看完了来介绍一下:先创建一个空对象newObj
,再通过for..in
遍历原对象上的属性,然后借助hasOwnProperty
规避原对象隐式具有的属性,接着将原对象obj
的属性复制给newObj
,最后返回新创建的对象newObj
。看完是不是心里有点底了。
深拷贝
讲完了浅拷贝再来聊一聊深拷贝吧。如果需要复制一个对象的所有属性,包括嵌套的对象和数组,我们需要使用深拷贝(Deep Copy)。 关于深拷贝与浅拷贝的差别就是拷贝后的对象不受原对象的影响。
常用深拷贝方法
1.JSON.parse(JSON.stringify(obj))
JSON.stringify()
方法可以将 JavaScript 对象转换成一个 JSON 字符串,而 JSON.parse()
则能将 JSON 字符串解析回 JavaScript 对象。但这个方法有几点要注意:
- 无法识别BigInt类型
- 无法拷贝 undefined,function,Symbol
- 无法处理循环引用
let obj ={
a:1,
b:{n:2},
c:'cc',
d:true,
e:undefined,
f:null,
g:function(){},
h:Symbol(1),
// i:123n
}
let newObj=JSON.parse(JSON.stringify(obj))
obj.a=2
console.log(newObj);
2. structuredClone()
structuredClone()
是JavaScript中的一个函数,用于创建一个深度拷贝的对象或值。这意味着原始对象和拷贝对象之间没有任何关联,修改其中一个对象不会影响到另一个。它也有几个注意点:
- 无法识别函数和Symbol类型
- 无法处理循环引用
let obj ={
a:1,
b:{n:2},
c:'cc',
d:true,
e:undefined,
f:null,
// g:function(){},
// h:Symbol(1),
i:123n
}
const newObj=structuredClone(obj)
obj.b.n=3
console.log(newObj);
手动实现深拷贝
了解一下上述两种方法,现在我们来手动实现一下深拷贝吧。手动实现深拷贝与浅拷贝大致方法差不多,但深拷贝和浅拷贝的区别是是否受原对象的影响,浅拷贝初略讲就是把引用类型在堆中的地址编号给复制给了新对象,而深拷贝是要将引用类型的在堆中的数据给复制到新对象。而要实现深拷贝最主要就是引用一个递归的方法。接下来看下面代码如何实现的吧:
let obj={
a:1,
b:{n:2}
}
function deepCopy(obj){
let newObj={}
for(let key in obj){
if(obj.hasOwnProperty(key)){
if(obj[key] instanceof Object){
newObj[key]=deepCopy(obj[key]) // 递归
}else{
newObj[key]=obj[key]
}
}
}
return newObj
}
let obj2=deepCopy(obj)
obj.b.n=20
console.log(obj2);
这段代码与浅拷贝的代码的区别就是多了一个if判断obj[key]
是否还是引用类型,然后调用了 deepCopy
函数自身,对 obj[key]
进行递归拷贝。
转载自:https://juejin.cn/post/7379149008358326281