0%

关于深拷贝

深拷贝是时常被提到的一个概念,今天来聊一聊深拷贝。

什么是深拷贝

说到深拷贝,就知道肯定有对应的浅拷贝,在理解什么是深拷贝之前,我们不妨先看下什么是浅拷贝。

当从一个变量向另一个变量复制引用类型的值时,同样也会将存储在变量对象中的值复制一份放到为新变量分配的空间中。不同的是,这个值的副本实际上是一个指针,而这个指针指向存储在堆中的一个对象。复制操作结束后,两个变量实际上将引用同一个对象。

上面这段话引用自《JavaScript高级程序设计 第三版》,这句话实际上解释了什么是浅复制。当我们把一个对象a赋值给一个变量b时,这时a和b实际指向的是一个对象。理解了浅复制,那么深复制的含义就很好理解了。

深拷贝的几种实现方法

1. 使用JSON序列化操作实现深拷贝

我们知道,JSON对象提供了两个静态方法。其中JSON.parse()用来把JSON字符串转换为对象,而JSON.stringify()则是把对象转换为JSON字符串,正好是一对互逆的操作。利用这两个方法,我们就可以实现深拷贝。

1
2
3
4
5
6
function deepClone(obj) {
return JSON.parse(JSON.strigify(obj))
}

var obj = {arr: [1, 2], person: {name: 'Jan'}
deepClone(obj}) // {arr: [1, 2], person: {name: 'Jan'}

通过上面的代码我们知道了JSON.parse(JSON.strigify(obj))这么简单的一句代码即可实现对象的深拷贝。但是这种方法是有缺陷的,当对象的属性值为一个函数或undefined时,JSON.stringify()会忽略这个属性。

function和undefined会被忽略

2. 利用for…in循环和递归实现深拷贝

for…in可以遍历对象的属性,所以我们在for…in循环里把对象的属性一个一个添加到预先声明的空对象里,这样就实现了对象的拷贝。当然,还要考虑到如果对象的属性还是对象,那么就需要递归的调用深拷贝函数,把属性值也为对象的属性也做一次拷贝,直到所有的属性都是基本类型为止,这样就完成了深拷贝。需要注意对对象和数组的拷贝操作是有区别的,看下面的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
var deepClone = function (obj) {
if (typeof obj !== 'object') {
throw new Error('arguments[0] must be an object')
}
var isArray = Array.isArray(obj)
var res = isArray ? [] : {}
for (var prop in obj) {
res[prop] = typeof obj[prop] === 'object' ? deepClone(obj[prop]) : obj[prop]
}

return res
}

var arr1 = [1, 2]
var arr2 = [3, 4]
var arr = [arr1, arr2]

var obj1 = {
name: 'xiaoMing'
}
var obj2 = {
name: 'xiaoHong'
}
var obj = {
obj1,
obj2
}

// 这里对深拷贝后的数组第一项做push操作,对应的arr1并没有随之改变,说明深拷贝成功
var arrDeep = deepClone(arr) // [[1, 2], [3, 4]]
arrDeep[0].push(3)
console.log(arr1) // [1, 2]

// 这里对深拷贝后的对象的obj1属性添加新属性,对应的obj1并没有随之改变,说明深拷贝成功
var objDeep = deepClone(obj) // {obj1: {name: 'xiaoMing'}, obj2: {name: 'xiaoHong'}}
objDeep.obj1.age = 10
console.log(obj1) // {name: 'xiaoMing'}

还有一点需要注意的是:for…in操作返回的是所有 可通过对象访问可枚举的属性,这其中也包括了原型上的可枚举属性。

1
2
3
4
var o = {}
o.__proto__.name = 'o'
// 原型上的可枚举属性也被拷贝了
deepClone(o) // {name: o}

注意上面的代码,如果使用第一种JSON序列化的方法的话,是无法拷贝原型上的可枚举属性的。

Object.assign()

Object.assign()方法用于将所有可枚举属性的值从一个或多个源对象(sources)复制到目标对象(target)。它将返回目标对象。这个方法我们平时经常会用到,它是浅拷贝。

1
2
3
4
5
6
7
var jan = {name: 'Jan', age: 18}
var niko = {name: 'Niko'}
var o1 = {a: jan, b: 1}
var o2 = {a: niko, b: 2, c: 2}
var o = Object.assign({}, o1, o2) // {a: {name: 'Niko'}, b: 2, c: 2}
o.a.name = 'Jan'
console.log(niko.name) // 'Jan'

从上面的代码也可以看出Object.assign是浅拷贝的,并且右边的源对象的属性会覆盖左边的源对象的属性,顺序是从右往左的。

jQuery.extend

jQuery实现了工具方法extend,这个方法传参和Object.assign类似,有两种用法,分别对应浅拷贝和深拷贝。浅拷贝$.extend(target, object1 [,objectN]),深拷贝$.extend([deep], target, object1 [,objectN]),当第一个参数传布尔值true时代表深拷贝。

关于$.extend的实现,这里先占个坑,有时间分析一下。

lodash的深拷贝

lodash也提供了浅拷贝和深拷贝,分别是_.clone()_.cloneDeep()。这里也先占个坑,以后有时间再分析。