浅拷贝和深拷贝的区别

188 阅读5分钟

一、浅拷贝和深拷贝的区别

1.1 什么是拷贝?

答:ctrl + c

ECMASciprt包括两个不同类型的值:基本数据类型和引用数据类型。

  • 基本数据类型:(值传递)(存储在栈内存中)

    常见的基本数据类型:Number,String, Boolean, Null, Undefined。

    不常见的基本数据类型:BigInt,Symbol。

  • 引用数据类型:Object,Array,Date,function,RegExp...。(地址传递)(存储在堆内存中)

总结区别:

内存分配的区别

  • 基本数据类型:

    存储在栈内存中的简单数据段,也就是说,它们的值直接储存在变量访问的位置。这是因为:这些原始类型占据的空间是固定的,所以可将他们存储在较小的内存区域 - 栈中。这样存储便于迅速查询变量的值。

  • 引用数据类型:

    储存在堆内存中的对象,也就是说,储存在变量处的值是一个指针(point),指向存储对象的内存地址。这是因为:引用值的大小会改变,所以不能把它放在栈中,否则会降低变量查询的速度。相反,放在变量的栈空间中的值是该对象存储在堆中的地址。地址的大小是固定的,所以把它存储在栈中对变量性能无任何负面影响。

访问机制区别

  • 基本数据类型:

    可以直接访问

  • 引用数据类型:

    不允许直接访问保存在堆内存中的对象的,所以在访问一个对象时,首先得到的是这个对象在堆内存的地址,然后再按照这个地址去获取这个对象中的值,这就是 按引用访问

复制变量时的区别

  • 基本数据类型:

    在将一个保存着原始值的变量复制给另一个变量时,会将原始值的副本赋值给新变量,此后这两个变量是完全独立的,他们只是拥有相同的value而已。

  • 引用数据类型:

    在将一个保存着原始值的变量赋值给另一个变量时,会把这个内存地址赋值给新变量。也就是说这两个变量都指向了内存中的同一地址,它们中任何一个做出的改变都会反映到另一个身上。(这里要理解一点的就是,复制对象时并不会在堆内存中新生成一个一模一样的对象,只是对了一个保存指向这个对象指针的变量而已)。

  • 浅拷贝实现方式:

    常见对象浅拷贝的方式:Object.assign()扩展运算符

//方式一、Object.assign()
    const obj1 = { name: '张三', info: { age: 18 } } ;
    const obj2 = Object.assign({}, obj1);
    
    obj2.name = '李四' ;
    obj2.info.age = 19 ;
    
    console.log(obj1) // { name: '张三', info: { age: 19} } 
    console.log(obj2) // { name: '李四', info: { age: 19 }
    
   

    
 //方式二、扩展运算符
     const obj2 = { ...obj1 }
     obj2.name = '李四' ;
     obj2.info.age = 19 ;
     
    console.log(obj1) // { name: '张三', info: { age: 19} } 
    console.log(obj2) // { name: '李四', info: { age: 19 }

上述例子可以看出当拷贝后的对象obj2.info.age发生改变obj1.info.age也发生了改变,因为info对象拷贝的是源对象指针。

  • 深拷贝实现方式:

    常见深拷贝的方式:JSON.parse(JSON.stringify(data))配合使用

//方式一、JSON.parse(JSON.stringify(data))
const obj1 = { name: '张三', info: { age: 18 } } ;
const obj2 = JSON.parse(JSON.stringify(obj1)) ;

obj2.name = '李四' ;
obj2.info.age = 19 ;

console.log(obj1) // { name: '张三', info: { age: 18 } } 
console.log(obj2) // { name: '李四', info: { age: 19 } }

//方式一弊端`
//  undefined、任意的函数以及 symbol 值,在序列化过程中会被忽略。
//  Date 日期调用了toJSON()将其转换为了string 字符串(Date.toISOString()),因此会被当做字符串处理。
//  NaN 和 Infinity 格式的数值及 null 都会被当做 null。
//  其他类型的对象,包括 Map/Set/WeakMap/WeakSet,仅会序列化可枚举的属性。
//  对包含循环引用的对象(对象之间相互引用,形成无限循环)执行此方法,会抛出错误。

//方式二、函数封装

let deepCopy = function (obj) { 
    // 只拷贝对象 
    if (typeof obj !== 'object') return; 
    // 根据obj的类型判断是新建一个数组还是对象 
        let newObj = obj instanceof Array ? [] : {}; 
        // 遍历obj,并且判断是obj的属性才拷贝 
        for (let key in obj) { if (obj.hasOwnProperty(key)) { 
        // 如果obj的子属性是对象,则进行递归操作,否则直接赋值 
            newObj[key] = typeof obj[key] === 'object' ? deepCopy(obj[key]) : obj[key];
        } 
    }
    return newObj; 
}

方式三、

//使用第三方库`Lodash`中的`_.cloneDeep()`。
Lodash 中文文档:  https://www.lodashjs.com/)https://www.lodashjs.com/docs/lodash.cloneDeep#_clonedeepvalue

知识拓展:

变量存储 (栈 & 堆)

  • 栈是内存中一块用于存储局部变量和函数参数的线性结构,遵循着先进后出的原则。数据只能顺序的入栈,顺序的出栈。当然,栈只是内存中一片连续区域一种形式化的描述,数据入栈和出栈的操作仅仅是栈指针在内存地址上的上下移动而已。

    较为经典的就是乒乓球盒结构,先放进去的乒乓球只能最后取出来。

    特点:轻量,不需要手动管理,函数调用时创建,调用结束则消失(出栈)。

  • 是堆内存的简称,堆是动态分配内存,内存大小不固定,也不会自动释放,堆数据结构是一种无序的树状结构,同时它还满足key-value键值对的存储方式;我们只用知道key名,就能通过key查找到对应的value。

    比较经典的就是书架存书的例子,我们知道书名,就可以找到对应的书籍;

    特点:速度稍慢、容量比较大;

内存分配&垃圾回收

  • 内存分配

    栈内存:线性有序存储,容量小,系统分配效率高。

    堆内存:首先要在堆内存新分配存储区域,之后又要把指针存储到栈内存中,效率相对就要低一些了。

  • 垃圾回收

    栈内存:变量基本上用完就回收了,相比于堆来说存取速度会快,并且栈内存中的数据是可以共享的。

    堆内存:堆内存中的对象不会随方法的结束而销毁,就算方法结束了,这个对象也可能会被其他引用变量所引用(参数传递)。创建对象是为了反复利用(因为对象的创建成本通常较大),这个对象将被保存到运行时数据区(也就是堆内存)。只有当一个对象没有任何引用变量引用它时,系统的垃圾回收机制才会在核实的时候回收它。