基本类型和引用类型
在开始讲解JavaScript的深拷贝和浅拷贝之前,先要认识JavaScript的两种基本数据类型。一种是基本类型(值类型),另外一种是引用类型。其中基本类型包括undefined
、null
、number
、string
和boolean
,这几种类型在内存中都有固定的大小和空间。引用类型包括object
,这种值的大小不固定,可以动态添加属性和方法,而基本类型则不可以。
基本类型的值保存在内存中的栈中,而引用数据类型的值保存在内存中的堆中,在栈内存中保存着指向堆内存的指针。如果此时你对栈或者堆这样的数据结构不太了解的话,没有关系,不会影响你对后面内容的理解。只要先记住栈和堆着两个名词就可以。
如图所示,这是一个引用数据类型,也就是对象(object)在内存中的保存方式。比如我们定义方式如下所示:
let Foo = {
words: "haha"
}
对象的内容保存在堆中,而对象的引用,也就是这里的Foo,保存在栈中,并指向对应的堆。
我们明白了这样的道理之后,就会清楚对于基本数据类型的值的复制,是对栈内存中的值直接进行复制,所以复制的就是值本身,相当于复制了一个副本。会在栈中开辟一块全新的内存。所以修改一个变量的值不会影响另外一个变量的值。所以对于基本数据类型而言,没有浅拷贝和深拷贝之分,或者说直接就是深拷贝。
引用类型浅拷贝
对于引用类型进行复制,如下代码所示,复制的并不是堆内存中的数据,而是指向堆中的栈内存的指针。如上面那个图所示,原先在标记为a的栈内存中有一个Foo变量,指向对应的堆内存中的数据,现在通过复制这个指针,在堆内存中新开辟一个内存空间,我们标记为c,其指向的和a是同样的堆内存数据。所以修改a和c都会导致堆内存中的数据发生改变。
let Foo = {
a: 3,
b: 4
}
let newFoo = Foo
newFoo.a = 5
之后输出的结果是:
由于Foo和NewFoo都指向同样的对象,所以改变NewFoo中的数据,Foo中的数据同样也会发生改变。那如何去改变这一点呢?让Foo和NewFoo中的数据不同,改变一个不会影响另外一个?我们先谈一下浅拷贝的解决办法。
浅拷贝可以使用Object.assign()
来实现。我们还是举上面的例子:
let Foo = {
a: 3,
b: 4
}
// let newFoo = Foo
Object.assign(newFoo, Foo)
newFoo.a = 5
这时候输出的结果是:
可以看出,Foo的结果并没有发生改变,达到了拷贝的目的。但这为什么又称之为浅拷贝呢?我们来看下面一个例子。
let Foo = {
a: {
c:1
},
b: 4
}
// let newFoo = Foo
Object.assign(newFoo, Foo)
newFoo.a.c=2
这时候输出结果是:
可以看出,此时c的结果发生了改变,已经变成2了。这就是浅拷贝的意思。只拷贝了最浅层的对象,当对象里面嵌套的对象发生改变时,其内部值也会发生改变,没有达到完全隔离的效果,只实现了浅层的拷贝。下面我们一起来看一下如何实现深层拷贝。
引用类型深拷贝
实现深层拷贝方式有两种,一种是通过JSON的方式,另外一种是通过类型判断和递归的方式。我们分别来实现一下。
- JSON方式
JSON有两个方法JSON.parse()
和JSON.stringify()
,前者可以将JSON字符串转换成JavaScript对象,后者可以将JavaScript对象转换成JSON字符串。
所以解决思路就是先将引用类型数据变成JSON,再将其从JSON变回来,这样经过这样一层转换,就会要求计算机从内存中重新开辟一块新的内存空间。Foo和newFoo之间就不会发生任何联系了。我们通过代码来去具体看一下。
let Foo = {
a: {
c:1
},
b: 4
}
let str = JSON.stringify(Foo)
let newFoo = JSON.parse(str)
newFoo.a.c = 5
此时Foo输出的结果是:
可以看到c并没有发生改变,实现了深拷贝。
- 通过递归的方式实现
之前JSON方式实现深拷贝就是靠着返回一个独立的对象来实现深拷贝,受此启发,如果要是我们自己来实现深拷贝的话,我们可以将原来的数据完全拷贝之后,然后返回一个独立的新对象,这样的话,也可以实现深拷贝。
如果要实现完全遍历的话,就要考虑Object的不同具体数据类型了,虽然{}
和[]
都是Object类型,但显然生成的对象是不一样的,当然,可能Object还可能有别的数据类型的样子,这里我们就以这两个为例,来去自己动手实现深拷贝。
那么首先需要写一个数据类型判断的函数:
let checkType = data => {
return Object.prototype.toString.call(data).slice(8, -1)
}
这里我们使用Object的toString方法来去判断data的数据类型,后面的slice是返回值结果进行一定的截取,保留我们想要的结果。
下面定义深拷贝函数:
let deepClone = target => {
let targetType = checkType(target)
let result
// 初始化操作
if (targetType === 'Object') {
result = {}
} else if (targetType === 'Array') {
result = []
} else {
// 都不是的话证明是基本数据类型,基本数据
// 类型只会有一个值,所以直接返回这个值就可以了
return target
}
// target不是基本类型,进入遍历
for (let i in target) {
let value = target[i]
let valueType = checkType(value)
if (valueType === 'Object' || valueType === 'Array') {
result[i] = deepClone(value) // 递归
} else {
// 是基本类型直接赋值
result[i] = value
}
}
return result
}
这样我们实现了自己定义的深拷贝。