深拷贝和浅拷贝都和对象的复制有关,它们是对象复制的两种不同方式。接下来就来看一下,浅拷贝是怎么个浅法,还有深拷贝深在哪里。
简单粗暴浅拷贝
来个栗子
先复制一个对象看看:
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] 都不会受到丝毫影响。
归纳一下
这就是深拷贝,深拷贝在遍历源对象属性的时候,遇到可遍历的,如对象和数组,会再深入遍历其属性,并进行拷贝。
总结
从上面的例子可以知道,深拷贝和浅拷贝,区别在于,在遍历源对象的时候,遇到可遍历的属性值,会不会去进行遍历。浅拷贝是直接将属性值复制过去,而深拷贝则会继续深入遍历并拷贝。