通俗易懂讲解JavaScript深拷贝和浅拷贝

基本类型和引用类型

在开始讲解JavaScript的深拷贝和浅拷贝之前,先要认识JavaScript的两种基本数据类型。一种是基本类型(值类型),另外一种是引用类型。其中基本类型包括undefinednullnumberstringboolean,这几种类型在内存中都有固定的大小和空间。引用类型包括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
}

这样我们实现了自己定义的深拷贝。

  • 4
    点赞
  • 35
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Einstellung

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值