谈一下JS的深浅拷贝

何为深浅拷贝

我们都知道我们的数据类型可以分为基本类型:

1
2
3
4
5
6
string
null
number
undefiend
boolean
symbol

而同样可以分出两个引用类型:

1
2
3
object
function
`

它们的区别在于,基本类型在堆栈中存的是value本身,而引用类型存的是指向堆中实际value的地址,称为引用句柄。
当我们拷贝我们的对象的时候,基本类型就比较简单,直接复制value就可以了。而引用类型就会有两个结果:

  1. 只复制了引用句柄,那么指向的value如果发生了改动,那么我们复制后对应的value也会跟随改变,因为实际上指向的是同一个value,这就叫浅拷贝
  2. 在堆中开辟一个新的位置,把被复制对象指向的value复制到新的位置中,复制对象再指向这个信息位置,这就叫深拷贝

浅拷贝

根据我们的定义,那能不能直接这样:

1
var newObj = oldObj

答案当然是不可以,这样的话,这本质上只是建立了一个新的变量指向了旧的对象,如果改变newObj中的value,那么连基本类型的存储value都会一并被改变。
因此浅拷贝是可以依靠创建一个新的对象的方式来传入目标的键值:

1
var newObj = Object.assign({}, oldObj)

这样的话虽然引用类型同样还是指向同一个引用对象,但是基本类型都已经拥有了属于自己的存储空间,本质上是分隔开了。

深拷贝

正如前面分析,深浅拷贝之间的区别就在于引用类型指向的是新的引用对象,还是依然指向被复制对象的引用对象。

很多文章书籍都介绍了一种方便的方法(例如《你不知道的javascipt》),就是利用JSON和字符串之间的转换:

1
2
3
4
5
6
7
var ref = {a: 1}
var oldObj = {
type: 'old',
data: ref
}

var newObj = JSON.parse(JSON.stringify(oldObj))

但是在实际开发的时候,我们会遇到一个问题:

1
2
3
4
5
6
7
8
var ref = {a: 1}
var oldObj = {
type: 'old',
data: ref
}
ref.parent = oldObj

var newObj = JSON.parse(JSON.stringify(oldObj))

那么就会得到一个TypeError:

1
Uncaught TypeError: Converting circular structure to JSON

因为oldObj中的对象形成了回路,JSON.stringify无法转换成功。

其实深拷贝是很依赖业务开发定义的,例如如果能保证对象中不造成引用回路的话,JSON转换的方法足够了,同样,我们需要的深拷贝可能只要第一层的深度或者全部都要深拷贝。从自身项目出发。

假设我们要做全量深拷贝的话,只会有一点问题的,这个问题后面再来谈,先来看下实现的代码:

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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
function deepClone(item) {
if (!item) { return item } // null, undefined values check

let types = [ Number, String, Boolean ]
let result

// 标准化类型,防止有类似new String('aaa')或 new Number('444') 的操作
types.forEach(function(type) {
if (item instanceof type) {
result = type( item )
}
})

if (typeof result == "undefined") {
// 如果是数组类型
if (Object.prototype.toString.call( item ) === "[object Array]") {
result = []
item.forEach(function(child, index, array) {
// 递归
result[index] = deepClone( child )
})
// Object类型
} else if (typeof item == "object") {
// 是不是dom
if (item.nodeType && typeof item.cloneNode == "function") {
result = item.cloneNode( true )
// 不含prototype,则是object
} else if (!item.prototype) {
// 是不是日期类型
if (item instanceof Date) {
result = new Date(item)
} else {
// 深层递归
result = {}
for (var i in item) {
result[i] = deepClone( item[i] )
}
}
} else {
// 是否有构造函数,是则为函数
if (false && item.constructor) {
// 不建议这样去做,原因在下面
result = new item.constructor()
} else {
result = JSON.parse(JSON.stringify(item))
}
}
} else {
result = item
}
}

return result
}

那么现在来讨论下拷贝objects时候会遇到的问题。当我们创建一个新对象的时候,会做一下操作:

1
2
var User = function(){}
var newuser = new User()

当然我们可以通过这种方式去拷贝,每个对象都会暴露一个构造器(constructor)属性,我们可以通过它去拷贝对象,但这种方式并非万无一失。我们同样可以使用一个简单的for in去遍历这个对象,但它同样会走向同一个方向 - 麻烦。上面代码有实现了使用构造器去拷贝一个对象,只是用了false来隐藏。

那么,为什么拷贝是一种痛苦呢? 首先,每个对象/instance 都可能有它自身的状态,我们不能在拷贝的时候确认这个对象的状态进而拷贝到我们的对象中去,否则会破坏这个状态。
另外,通过构造器去拷贝的时候,参数依赖也是一个障碍。我们很难去确认,创建对象的人有没有传入像下面那样的参数:

1
2
3
new User({
bike: someBikeInstance
})

遇到这种情况,someBikeInstance可能是从相同的上下文中创建,也可能在拷贝方法中无话获知的上下文中创建。

因此对于基于function的类的拷贝,我们目前也最好也使用JSON.parse(JSON.stringify(...))来进行拷贝。