JavaScript——一文搞懂深浅拷贝

JavaScript——深浅拷贝

开发中我们经常需要复制一个对象。如果直接用赋值会有下面问题。

const person = {
    name: 'wjr',
    age: 18
}
const person2 = person
console.log(person2);  // {name: 'wjr', age: 18}
person2.name = 'WJR'
// 修改了person2,person也被改变了
console.log(person2);  // {name: 'WJR', age: 18}
console.log(person);  // {name: 'WJR', age: 18}

当改变新对象时,原对象也跟着发生变化,以上情况被称为赋值(Copy),当我们修改新对象时,不希望原对象也发生改变,此时我们就需要了解一下深拷贝与浅拷贝,以及其中的区别。

一、浅拷贝(Shallow Copy)

1.1 概念

概念:创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,那么拷贝的就是基本类型的值;如果属性是引用类型(数组、对象),拷贝的就是内存地址。因为基本类型以及引用类型的地址都存放在内存的栈中,因此浅拷贝可以认为是拷贝栈中的值,即在内存栈中开辟新的空间存储基本类型和引用类型的地址。

例如:

let obj = {
    a: 1,
    b: {
        c: 2
    }
}

那么obj的浅拷贝就可以看作是在内存栈中开辟新空间存放a的值和b的地址,b的地址指向内存堆空间的c的值。

1.2 使用场景

1.2.1 浅拷贝对象
  1. Object.assign()

Object.assign()用于将所有可枚举属性的值从一个或多个源对象复制到目标对象。它将返回目标对象。

const target = {}
const source = { a: 1, b: { c: 1 } }
console.log(Object.assign(target, source));  // { a: 1, b: { c: 1 } }
console.log(target);  // { a: 1, b: { c: 1 } }
console.log(source);  // { a: 1, b: { c: 1 } }

接着当修改对象source中的对象b的属性c时,可以看到复制结果target也会随之改变,如下代码,可见Object.assign()实现的是浅拷贝。

const target = {}
const source = { a: 1, b: { c: 1 } }
console.log(Object.assign(target, source)); // { a: 1, b: { c: 5 } }
console.log(target);  // { a: 1, b: { c: 5 } }
console.log(source);  // { a: 1, b: { c: 5 } }
source.b.c = 5
  1. 扩展运算符...Object

扩展运算符的实际效果和Object.assign()一样,如下代码

const source = { a: 1, b: { c: 1 } }
const target = { ...source }
console.log(source);  // { a: 1, b: { c: 1 } }
console.log(target);  // { a: 1, b: { c: 1 } }

当修改对象source中的对象b的属性c时,也可以看到复制结果target也会随之改变。如下

const source = { a: 1, b: { c: 1 } }
const target = { ...source }
console.log(source);  // { a: 1, b: { c: 5 } }
console.log(target);  // { a: 1, b: { c: 5 } }
source.b.c = 5
1.2.2 浅拷贝数组
  1. Array.prototype.slice()

slice()方法返回一个新的数组对象,这一对象是一个由beginend决定的原数组的浅拷贝,原始数组不会被改变。

const a = [0, 1, [2, 3]]
const b = a.slice()  // 不填参数,默认全部
console.log(a);  // [0, 1, [2, 3]]
console.log(b);  // [0, 1, [2, 3]]

当修改数组b内的数组时,a也随之发生变化,如下代码,可见Array.prototype.slice()实现的是浅拷贝。

const a = [0, 1, [2, 3]]
const b = a.slice()  // 不填参数,默认全部
console.log(a);  // [0, 1, [5, 3]]
console.log(b);  // [0, 1, [5, 3]]
b[2][0] = 5
  1. Array.prototype.concat()

二、深拷贝(deep Copy)

2.1 概念

将一个对象从内存中完整的拷贝一份出来,从堆内存中开辟一个新的区域存放新对象,且修改新对象不会影响原对象。

深拷贝会拷贝所有的属性,并拷贝属性指向的动态分配的内存。当对象和它所引用的对象一起拷贝时即发生深拷贝。深拷贝相比于浅拷贝速度较慢并且花销较大。拷贝前后两个对象互不影响。

2.2 使用场景

2.2.1 递归实现深拷贝
// 原对象
const obj = {
    uName: 'wjr',
    age: 18,
    movieArr: ['Scent of a Woman', 'Gone with the Wind'],
    minObj: {
        one: 1,
        two: 2
    }
}
const newObj = {}
// 定义实现深拷贝的函数
function deepClone(newObj, oldObj) {
    for (let k in oldObj) {
        if (oldObj[k] instanceof Array) {
            newObj[k] = []
            deepClone(newObj[k], oldObj[k])
        } else if (oldObj[k] instanceof Object) {
            newObj[k] = {}
            deepClone(newObj[k], oldObj[k])
        }
        else {
            newObj[k] = oldObj[k]
        }
    }
}
// 调用深拷贝函数
deepClone(newObj, obj)
// 修改拷贝后的对象
newObj.age = 19
newObj.movieArr[1] = 'Titanic'
newObj.minObj.one = 3
// 输出新旧对象以对比修改新对象旧对象是否变化
console.log('原对象:', obj);
console.log('拷贝后再经修改的对象', newObj);

在这里插入图片描述

2.2.2 JSON.parse(JSON.stringify(object))

该方法的原理是:JSON.stringify()将JSON对象转换为JSON字符串,JSON字符串作为简单数据类型将在内存栈中开辟新空间并存放,接着,JSON.parse()将已在内存栈开辟新空间的JSON字符串转换为JSON对象,并且将地址存放在原先在内存栈中开辟的新空间,地址所指向的值存放在内存堆中新开辟的空间。拷贝后的对象不和原对象共用同一内存栈和内存堆空间,因此实现了原对象与新对象互不影响的深拷贝。下面我们通过代码更深刻的体会这行代码的巧妙之处。

另外,该方法对数组实现的拷贝效果和对对象相同。

// 原对象
const obj = {
    uName: 'wjr',
    age: 18,
    movieArr: ['Scent of a Woman', 'Gone with the Wind'],
    minObj: {
        one: 1,
        two: 2
    }
}
// 新对象
const newObj = JSON.parse(JSON.stringify(obj))
console.log('原对象:', obj);
console.log('新对象:', newObj);

拷贝结果

在这里插入图片描述

当修改拷贝的对象内的对象,不改变原对象,如下代码:

// 原对象
const obj = {
    uName: 'wjr',
    age: 18,
    movieArr: ['Scent of a Woman', 'Gone with the Wind'],
    minObj: {
        one: 1,
        two: 2
    }
}
// 新对象
const newObj = JSON.parse(JSON.stringify(obj))
// 修改拷贝后的对象
newObj.age = 19
newObj.movieArr[1] = 'Titanic'
newObj.minObj.one = 3
// 输出新旧对象以对比修改新对象旧对象是否变化
console.log('原对象:', obj);
console.log('拷贝后再经修改的对象', newObj);

在这里插入图片描述

如上实现了和递归实现深拷贝相同的效果,但是该方法有以下几个问题:

  1. 会忽略undefined

  2. 会忽略symbol

  3. 会忽略函数

    // undefined、symbol和函数这三种情况会直接忽略
    const obj = {
        name: 'wjr',
        a: undefined,
        b: Symbol('wjr'),
        c: function () {
    
        }
    }
    const newObj = JSON.parse(JSON.stringify(obj))
    console.log('原对象:', obj);
    console.log('新对象:', newObj);
    

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jCTyBuYO-1679822299973)(C:\Users\22706\AppData\Roaming\Typora\typora-user-images\image-20230326170712008.png)]

  4. 循环引用会报错

    let obj = {
        a: 1,
        b: {
            c: 2,
            d: 3
        }
    }
    obj.a = obj.b;
    obj.b.c = obj.a;
    
    let b = JSON.parse(JSON.stringify(obj));
    // Uncaught TypeError: Converting circular structure to JSON
    
  5. 不能正确处理new Date()

    const nowTime = new Date()
            const newTime = JSON.parse(JSON.stringify(nowTime))
            console.log('原对象:', nowTime);
            console.log('新对象:', newTime);
    

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5XxOhZT1-1679822299973)(C:\Users\22706\AppData\Roaming\Typora\typora-user-images\image-20230326171146041.png)]

    解决方法:转成时间戳

    const nowTime = (new Date()).valueOf()
    const newTime = JSON.parse(JSON.stringify(nowTime))
    console.log('原对象:', nowTime);
    console.log('新对象:', newTime);
    

    在这里插入图片描述

  6. 不能处理正则

    const obj = {
        name: 'wjr',
        a: /'123'/
    }
    const newObj = JSON.parse(JSON.stringify(obj))
    console.log('原对象:', obj);
    console.log('新对象:', newObj);
    

    在这里插入图片描述

2.2.3 lodash.__cloneDeep()

参考文档: JavaScript 实用工具库——lodash中文文档

三、区别

和原数据是否指向同一对象第一层数据为基本数据类型原数据中包含子对象
赋值改变会使原数据一同改变改变会使原数据一同改变
浅拷贝改变会使原数据一同改变改变会使原数据一同改变
深拷贝改变会使原数据一同改变改变会使原数据一同改变

参考链接:详细解析赋值、浅拷贝、深拷贝

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值