深拷贝和浅拷贝常见误区

577 阅读5分钟

这是我参与8月更文挑战的第3天,活动详情查看:8月更文挑战

前言

无论是日常开发还是面试,深拷贝和浅拷贝遇到得概率都是很大的,但是真正能把这些知识理解的很明白的人却很少,包括我们经常看到的帖子都有很多错误。

本文主要讲解什么是深拷贝、什么是浅拷贝、深拷贝与浅拷贝的区别、纠正一些帖子常见的错误。

看前须知

只有引用类型数据才有谈论拷贝的必要,对于基础类型数据永远就是赋值,不存在拷贝

常见的错误理解

  • 引用赋值就是浅拷贝

     let a = { age: 18 }
     let b = a
     a.age=999
     console.log(b.age)  //999
    

    很多人认为b=a的赋值是浅拷贝,因为网上流程了这样的理论,浅拷贝只是复制引用类型数据在栈中的地址,单纯的理解这句话是没有错的,但是却不完全对!因为浅拷贝的前提是:先在堆空间创建一个对象,再对原始对象的属性进行复制,如果是引用类型就复制引用类型数据在栈中的地址。可以看出,赋值肯定不属于浅拷贝,因为b变量没有新建对象,完全是和a 变量占用一个对象的。

  • 浅拷贝就是只对对象的第一层进行拷贝

    我一开始也是这样理解的,并且这样理解也不会有什么错,但是浅拷贝本质原理本身不是这样的,这样理解只能算歪打正着。引用沸点[艺术伟大已极]大佬给我的解释,个人感觉很完美:

    浅拷贝和深拷贝的区别不在于拷贝的层数差异,要区分浅拷贝和深拷贝就必须要了解原始类型的值和引用类型的值在赋值时的行为差异。 原始类型的值是存储在栈中的,程序可以直接从栈中获取到原始类型的值。 引用类型的值是存储在堆中的,不过它同时也在栈中开辟了一块内存用来存储该值在堆中的地址。程序在调用该引用类型的变量时,会先查找栈,再根据栈的值(该值是一个指针)查找到堆,最后从堆中取到值。 无论是原始类型的值,还是引用类型的值,在赋值行为中,都是将栈中存储的值拷贝给别人,但是原始类型和引用类型在赋值时之所以会有行为差异,就是因为它们在栈中存储的值不同,前者把值本身存储在栈中,后者把指向值的地址存储在栈中。 因此原始类型的值发生赋值行为时,就是将值复制一份传递出去,称为「值传递」。 引用类型的值在发生赋值行为时,就是将指向值的地址复制一份传递出去,称为「引用传递」。

    了解到这里,你大概就会理解,浅拷贝描述的就是「引用传递」这种行为,深拷贝描述的就是「值传递」这种行为。要对{}进行深拷贝,就是要将{}在堆中的值也复制一份传递出去,而这和”对象的层次“无关。

    个人理解: “浅拷贝拿栈里的值(引用类型数据拿地址或基本类型数据拿值),深拷贝拿的永远是真实值(引用类型数据拿堆里面的值或基本类型数据拿值)

赋值、浅拷贝、深拷贝的区别

赋值
 let a = { age: 18 }
 let b = a

当我们把一个对象赋值给一个新的变量时,赋的其实是该对象的在栈中的地址,而不是堆中的数据。也就是两个对象指向的是同一个存储空间,无论哪个对象发生改变,其实都是改变的存储空间的内容,因此,两个对象是联动的,会互相影响。

浅拷贝

先在堆空间创建一个对象,再对原始对象的属性进行复制,如果是引用类型就复制引用类型数据在栈中的地址,如果是基本类型就拿基本类型的值。

 let a = { age: 18, info: { name: "李白" } }
 ​
 let b = {}
 for (const key in a) {
     b[key] = a[key]
 }
 a.age = 999
 a.info.name = "杜甫"
 ​
 console.log(b.age)  //18
 console.log(b.info.name)  //李白

如果原始对象的属性是引用类型,那么只会复制地址,所以如果这个属性改变,也会影响浅拷贝过后的数据

深拷贝

将原始对象所有的属性及其子属性全部复制到新建的堆区域中;无论怎么去修改原始数据都不会影响拷贝后的数据。

未命名文件.png

浅拷贝的实现方式

1. Object.assign() 【常用】
 let a = { age: 18 }
 let b = Object.assign({}, a)

一定要注意Object.assign不能放a对象,如果放a对象,那么就是赋值了

2. 手写循环赋值
 let a = { age: 18 }
 let b = {}
 for (const key in a) {
     b[key] = a[key]
 }
3.展开运算符...
 let a = { age: 18 }
 let b = {...a}

ES6的语法,如果要兼容老的浏览器不建议

4.concat数组方法
 let a = [1,2,{ age: 18 }]
 let b = [].concat(a)
5.slice数组方法
 let a = [1,2,{ age: 18 }]
 let b =  a.slice()

深拷贝的实现方式

1.JSON.parse(JSON.stringify()) 【常用】
 let a = { age: 18 }
 let b = JSON.parse(JSON.stringify(a))

虽然它不能处理属性值为函数、正则、undefined,但是在日常开发过程中完全足够了

2.手写递归
 function clone(target) {
     if (typeof target === 'object') {
         let cloneTarget = {};
         for (const key in target) {
             cloneTarget[key] = clone(target[key]);
         }
         return cloneTarget;
     } else {
         return target;
     }
 };

这里书写了最简单的方法,当然不止这些,比如要考虑数组、函数、null、处理循环引用、处理栈溢出等问题。

如果关于这块如有疑惑,请仔细阅读ConardLi大佬如何写出一个惊艳面试官的深拷贝?这篇文章。

参考文章:

浅拷贝与深拷贝

js 深拷贝 vs 浅拷贝