深拷贝和浅拷贝都和对象的复制有关,它们是对象复制的两种不同方式。接下来就来看一下,浅拷贝是怎么个浅法,还有深拷贝深在哪里。
简单粗暴浅拷贝
来个栗子
先复制一个对象看看:
let a = {
name: 'a',
age: 99,
car: {
color: 'blue'
}
}
let b = {}
Object.keys(a).forEach((key) => {
b[key] = a[key]
})
console.log(b)
// {
// name: 'a',
// age: 99,
// car: {
// color: 'blue'
// }
// }
首先声明了一个对象 a
还有一个空对象 b
. 然后遍历对象 a
的可枚举属性,将属性和属性值一一分配给 b
. 这里,实现了对象的复制。
接下来,对 b
进行一些操作:
b.name = 'b'
b.car.color = 'red'
console.log(b)
// {
// name: 'b',
// age: 99,
// car: {
// color: 'red'
// }
// }
console.log(a)
// {
// name: 'a',
// age: 99,
// car: {
// color: 'red'
// }
// }
上面的代码,将 b
的 name
属性值变成 'b'
, 然后再将 b
的 car
属性的 color
变为 'red'
.
打印 b
,发现这些改动都预期发生了。
打印 a
, 出现了一些意料之外的事情。a
的 name
仍然是 'a'
,然而 a
的 car
属性的 color
属性却发生了变化,变得和刚刚赋给 b
的一样了,成了 'red'
.
这是怎么回事呢?
剧情解析
时光稍微倒流一下,回到复制对象的地方看一看。复制的关键代码:
Object.keys(a).forEach((key) => {
b[key] = a[key]
})
代码把 a
的可枚举属性悉数分配给 b
.
分配 car
属性的时候,慢动作细节即为:
b['car'] = a['car']
a['car']
的值是一个对象,准确地说,a['car']
的值是一个指针,指向了对象 {color: 'blue'}
。
在 JavaScript 当中,把对象赋给一个值,实际上是把对象的内存地址赋给这个值,然后,这个值就指向了该对象。对象是引用类型,不同于基本类型,引用类型存放在堆中。
当把 a['car']
的值赋给 b['car']
之时,其实是把这个指针复制了一份,然后赋给 b['car']
,最终, b['car']
也指向了同一个对象。
后来执行 b.car.color
对 b.car
指向的对象进行操作,a.car
也产生了变化,因为它们指向的就是同一个对象。
上面这个例子,就属于浅拷贝。
归纳一下
浅拷贝,简单粗暴,只管复制,不考虑属性值是不是指向对象。
可能上面这个归纳,在这个时候,还是有点模糊,感觉不甚清晰,接下来,来个深拷贝对比一下。
钻牛角尖深拷贝
来个栗子
先上代码:
function deepCopy (target, source) {
Object.keys(source).forEach((key) => {
if (getType(source[key]) === 'Object') {
target[key] = {}
deepCopy(target[key], source[key])
} else if (getType(source[key]) === 'Array') {
target[key] = []
deepCopy(target[key], source[key])
} else {
target[key] = source[key]
}
})
return target
}
// Get type of parameter val.
function getType(val) {
let reg = /^\[object\s(\w*)\]$/
return reg.exec(Object.prototype.toString.call(val))[1]
}
var a = {
name: 'a',
age: 99,
car: {
color: 'blue'
}
}
let b = {}
deepCopy(b, a)
console.log(b)
// {
// name: 'a',
// age: 99,
// car: {
// color: 'blue'
// }
// }
上面的代码实现了一个深拷贝函数 deepCopy
. 这个函数接受两个参数,target
是目标对象,source
是源对象,函数内将源对象的属性悉数拷贝到目标对象。
函数中首先遍历源对象的属性,分三种情况来处理,分别是当属性值指向对象,指向数组,或者是其他情况。
第一种情况,当 source[key]
指向对象时,先赋一个空对象给 target[key]
,然后再以 target[key]
这个空对象为 target
参数,以 source[key]
为 source
参数,递归调用 deepCopy
.
第二种情况,当 source[key]
指向数组,就赋一个空数组给 target[key]
,接下来以 target[key]
作为 target
参数,以 source[key]
为 source
参数,递归调用 deepCopy
.
第三种情况,当 source[key]
不是对象也不是数组,那么直接把 source[key]
的值赋给 target[key]
.
遍历结束,deepCopy
返回 target
.
接着,声明了函数 getType
, 就是 deepCopy
里面用到的判断数据类型的函数。getType
接受一个参数 val
, 并返回 val
的类型。函数内利用 Object.prototype.toString
得到 val
的类型,但这样得到的是类似 '[object String]'
以及 '[object Array]'
这样的字符串,还需要加工一下,才能拿到代表类型的字符串,所以声明了一个正则表达式,来进行从 '[object String]'
提取出 'String'
这样的工作。
然后,声明对象 a
, 接着,声明空对象 b
, 再执行 deepCopy(b, a)
将 a
深拷贝到 b
.
console.log(b)
可知 b
已经成功拿到 a
的可枚举属性。
接下来,对 b
进行一些操作:
b.name = 'b'
b.car.color = 'red'
console.log(b)
// {
// name: 'b',
// age: 99,
// car: {
// color: 'red'
// }
// }
console.log(a)
// {
// name: 'a',
// age: 99,
// car: {
// color: 'blue'
// }
// }
像第一个例子一样,将 b
的 name
属性赋值为 'b'
, b
的 car
属性的 color
赋值为 'red'
.
打印 b
, 发现改变已经生效。
打印 a
, 可见 a
没有像上一个例子那样发生改变。
剧情解析
在这个例子中,当遍历到 a[car]
属性的时候,发现属性值指向的是一个对象,就将一个空对象赋给 b[car]
, 再把 a[car]
所指向的对象当做 source
, b[car]
指向的空对象当做 target
, 执行 deepCopy
操作,将 a[car]
所指的对象属性拷贝给 b[car]
所指的空对象。这个时候, a[car]
和 b[car]
所指的并非同一个对象,所以,任凭怎么操作 b[car]
, a[car]
都不会受到丝毫影响。
归纳一下
这就是深拷贝,深拷贝在遍历源对象属性的时候,遇到可遍历的,如对象和数组,会再深入遍历其属性,并进行拷贝。
总结
从上面的例子可以知道,深拷贝和浅拷贝,区别在于,在遍历源对象的时候,遇到可遍历的属性值,会不会去进行遍历。浅拷贝是直接将属性值复制过去,而深拷贝则会继续深入遍历并拷贝。