三个概念:引用赋值、浅拷贝和深拷贝
引用赋值:定义一个变量,给该变量赋值(引用类型)的过程,实际上是在内存中开辟一块空间存放该引用类型值,这块被开辟出来的空间有一个内存地址如:0x0010001,然后该内存地址赋值给定义的变量。当使用该变量时,它会顺着内存地址找到内存中真正的内容。
如图,定义一个变量obj1,并给obj1赋值一个对象,再定一个变量obj2,并把obj1赋值给obj2。 这过程中在内存中开辟一块空间,然后将该空间地址赋值给变量,将obj1赋值给obj2也就是把该内存地址赋值给obj2,当改变obj2时,会顺着内存地址找到内存中真正的内容去修改它。
我们没有修改obj1,但是打印输出obj1,会发现obj1输出的内容却改变了,这就是引用赋值会造成的影响。
浅拷贝:浅拷贝时,在堆内存中开辟一块新的内存空间,这块新的内存空间保存的数据包含属性与源数据属性完全一致,但是从字面意思就能看它只能拷贝浅层次的数据属性。
如下图所示:定义一个obj1对象,它的值是一个对象且该对象中的某个属性的值也是一个对象。 那么保存在堆内存中的体现是会开辟出两块空间,一块空间保存的书obj1的数据,属性值为非引用类型的则直接赋值,属性值为引用类型则保存引用地址,指向实际的数据内容(内存地址:x010111)
当我们将obj1拷贝到obj2,在堆内存中会为obj2开辟一块空间,存放实际数据,给obj2赋值引用地址。如此当修改obj1.name
时,obj2不会受影响。但是如果修改obj1.info.job
,此时输出obj2,会发现obj2的info属性也发生了改变
深拷贝:理解了浅拷贝,深拷贝就也很容易理解了,顾名思义深拷贝就是完全的复制拷贝一份,除了源数据的基本类型值属性外,源数据的引用类性值也会在堆内存新开辟空间存放,从最外层开始查找一直到最里层。
深拷贝的几种方法比较
JSON转换
在实际项目中,大部分深拷贝使用需求JSON转换的方式就足够了,借助JSON.stringify()
和JSON.parse()
这两个方法就能符合我们大多数的项目需求。 举个例子:以下代码,任意修改obj1或者obj2的属性值,两者都不会受到影响
var obj1 = {
name:'赵老四',
age:20,
info:{
job:'前端工程师',
time:3
}
}
const obj2 = JSON.parse(JSON.stringify(obj1))
console.log(obj2)
obj1.info.job = '转行java开发'
obj1.age = 21
console.log(obj1,obj2)
但是JSON转换的方式并不能完美的深拷贝出源数据,当元数据中属性有函数、正则、时间对象、undefined时,它并不能拷贝到。它会忽略属性值为函数、undefined的属性,遇到属性值为时间对象,它会解析成时间字符串,遇到正则表达式会解析为空对象
var obj1 = {
name:'赵老四',
age:20,
date:new Date(),
func:function(){},
nund:undefined,
reg:/\.js/
}
const obj2 = JSON.parse(JSON.stringify(obj1))
我们知道JSON转换的方式可以做到深拷贝的效果,但是它为什么可以呢?我们知道深拷贝的本质是在堆内存中开辟空间存放实际数据,那么使用JSON转换的过程中是不是在堆内存中开辟了这样一块空间呢?
MDN上说JSON.stringify()
和JSON.parse()
就是序列化和反序列化的过程,序列化的作用是存储和传输,将一个javascript对象通过JSON.stringify()
序列化成JSON字符串,然后再通过JSON.parse()
对JSON字符串反序列化返回一个新的js对象
递归实现深拷贝
在用递归实现深拷贝之前,我们先用遍历的方式来实现一个浅拷贝,如下,obj1是可遍历对象,创建一个新的对象obj2,遍历obj1,将需要克隆对象的属性依次添加到新对象obj2上,这就实现了一个最基础版本的拷贝
var obj1 = {
name:'赵老四',
age:20,
info:{
job:'前端工程师'
}
}
const obj2 = {}
for(var k in obj1){
obj2[k] = obj1[k]
}
但前面所说,这种并没有将源数据中引用类型的属性完全拷贝,上面例子中obj1内有一个info
属性是引用类型的值。要完成深层次的拷贝,就需要利用递归去赋值所有层级的属性,直到层级属性值为基本类型为止。我们将上面代码改装成一个递归函数
function recursion(obj){
if(typeof obj === 'object'){
let res = {}
for(const k in obj){
res[k] = recursion(obj[k])
}
return res
}else{
return obj
}
}
如上代码,只是考虑了属性值为对象的类型,假如有属性值为数组类型的源数据通过它去拷贝会发生什么事情,我们不妨来测验一下。
var obj1 = {
name:'赵老四',
age:20,
arr:[1,2,3],
info:{
job:'前端工程师'
}
}
const obj2 = recursion(obj1) // 拷贝生成新对象obj2
console.log(Array.isArray(obj1.arr),Array.isArray(obj2.arr)) //输出obj1和obj2是否为数组
打开控制台可以看到输出的结果,obj2的arr属性不是数组,而是一个键值对形式,这是因为我们在递归函数中并没有对数组类型的值做处理,我们要做的是在遍历源数据的每一项对其进行类型判断,然后根据其类型进行不同决定接收值的变量是何种类型
继续对其进行改造优化如下,既可对对象进行遍历拷贝,也可以对数组进行遍历拷贝
function recursion(obj){
if(typeof obj === 'object'){
let res = Array.isArray(obj) ? [] : {}
for(const k in obj){
res[k] = recursion(obj[k])
}
return res
}else{
return obj
}
}