前端面试之深浅拷贝

堆和栈的主要区别

其实深拷贝和浅拷贝的主要区别就在于其内存中的存储类型不同.

堆和栈都是内存中划分出来用于存储的区域.

栈会自动扥配内存空间,由紫铜自动释放
堆则是动态分配的内存,大小不定也不会自动释放.

js数据类型

js 的数据类型大方向上来说有两种:

  1. 基本数据类型(String, Number, Boolean, Null, Undefined,Symbol)
  2. 引用数据类型(Array,Object)

当一个变量存放基本数据类型时与复杂的数据类型时分别存在以下的特点:

数据类型特点

  • 基本数据类型的特点:变量直接将基本数据类型的值存储在栈(stack)内存中,基本数据类型值不可变.
  • 引用数据类型的特点:变量将引用数据类型的引用(可以看作是地址)存储在栈(stack)内存,而对象本身存放在堆内存里,在栈中引用指向堆中的对象。

其实当解释器寻找引用值时,会首先检索其在栈中的地址引用,取得地址后从堆中获得对象。 ​

其实当解释器寻找引用值时,会首先检索其在栈中的地址引用,取得地址后从对中获得对象.
如下图:
在这里插入图片描述

基本数据类型值不可变

通过上面的分析,我们可以知道的是基本数据类型存放在栈中,引用数据类型存放堆中.

JavaScript中的原始值(null,undefined,布尔值,数字和字符串)与对象(包括数组和函数)有这根本却别.

原始值是不可更改:任何方法都无法更改一个原始值.对于数字和布尔值来说就不那么明显了,因为字符串看起来像由字符串组成的数组,我们期望可以通过指定索引来假改字符串中的字符.

实际上,javascript 是禁止这样做的。字符串中所有的方法看上去返回了一个修改后的字符串,实际上返回的是一个新的字符串值。

例如下面的代码:

var str = 'abc';
str[1] = 'f';
console.log(str);   //abc

后面会写一篇博客专门介绍为什么这些数据不可变

基本类型的比较是值的比较

    var a = 1;
    var b = 1;
    console.log(a === b); //true

比较的时候最好使用严格等,因为 == 是会进行类型转换的,比如:

    var a = 1;
    var b = true;
    console.log(a == b);//true

引用数据类型值可变

引用类型(object)是存放在堆内存中的,变量实际上是一个存放在栈内存的指针(js中实际上没有指针这个概念,为了理解我称他为指针),这个指针指向堆内存中的地址。每个空间大小不一样,要根据情况开进行特定的分配,例如。

var person1 = {name:'why'};
var person2 = {name:'gh'};
var person3 = {name:'xxg'};

在这里插入图片描述
引用类型的值是可变的.如下:

    let person = {
      name : 'why'
    };
    person['name'] = 'gh';
    console.log(person) //{name :'gh'}

    let arr = [1,2,3,4];
    arr[3] = 5;
    console.log(arr);   //[1,2,3,5]

引用类型的比较是引用的比较

    var a = [1,2,3];
    var b = [1,2,3];
    console.log(a === b); // false

虽然变量 a 和变量 b 都是表示一个内容为 1,2,3 的数组,但是其在内存中的位置不一样,也就是说变量 a 和变量 b 指向的不是同一个对象,所以他们是不相等的。
在这里插入图片描述

传值与传址

了解了基本数据类型和引用数据类型的基本区别之后,我们很快就能明白传值与传址的区别了.

在我们进行赋值操作的时候,基本数据类型的赋值(=)是在内存中新开辟一段栈内存,然后再把值赋值到新的栈中.

    var a = '李华';
    var b = a;
    console.log(a,b);  //李华 李华

    b = '韩梅梅';
    console.log(a,b)  //李华 韩梅梅

在这里插入图片描述
在这里插入图片描述
所以说,基本类型的赋值的两个变量是两个独立相互不影响的变量。

但是引用类型的赋值是传址。只是改变指针的指向,例如,也就是说引用类型的赋值是对象保存在栈中的地址的赋值,这样的话两个变量就指向同一个对象,因此两者之间操作互相有影响。例如:

var a = {}; // a保存了一个空对象的实例
var b = a;  // a和b都指向了这个空对象

a.name = 'jozo';
console.log(a.name); // 'jozo'
console.log(b.name); // 'jozo'

b.age = 22;
console.log(b.age);// 22
console.log(a.age);// 22

console.log(a == b);// true

在这里插入图片描述(懒癌晚期,直接拿的别人的图)

深拷贝和浅拷贝

引入深浅拷贝

我们先来看看下面的代码:

var a = 5;
var b = a;

var obj = {name : 'gh'};
var obj1 = obj;

对于以上的代码,变量的内存分配是在栈中,所以变量不可以存放堆中的对象,所以赋值就会出现两种情况:

基本数据类型:将变量num的值拷贝一份存储在numCopy中

赋值数据类型:将变量obj中直接存储的引用拷贝一份存储在objCopy中

对于上面的代码,变量的内存分配是在栈中,所以变量不可以存放对中的对象,所以赋值就会有下面两种情况:

  1. 基本数据类型 : 将变量a的值拷贝一份存储在b中
  2. 引用数据类型:将变量obj中存值的引用拷贝一份存储在obj1中.

如下图:
在这里插入图片描述

所以Number类型的变量num通过简单的赋值得到了两份值(在栈内有不同的存储空间),而复杂数据类型obj通过赋值仅仅是得到了两份引用(在栈内有不同的内存空间),而实际的对象只有一份(在堆内存中是同一个值)

所以我们可以知道,要拷贝一份复杂数据类型远没有我们想象的那样简单。

这也就引出了我们今天的话题,深拷贝和浅拷贝。

值得我们注意的是,我们平时想下面的操作只能叫做赋值,和深浅拷贝完全没有关系,只有当我们真的需要两份相同的值而又对他们进行不同的操作的时候才会有深浅拷贝的说法.

var a = 5;
var b = a;
//这仅仅是一个赋值操作

我们应该注意的一点,深拷贝和浅拷贝只是针对与Object 和 Array 这样的引用数据类型的,而简单数据类型由于情况比较简单,就是拷贝一份,所以不存在深拷贝和浅拷贝的说法

赋值浅拷贝辨析

  • . 赋值
    • 当我们把一个对象赋值给另外一个新的变量的时候,赋的其实是该对象在栈中的地址,而不是存储在堆中的数据
    • 所以也就是说两个对象指向的是同一个存储空间,无论哪个对象发生改变,其改变的都是存储空间中的对象本身.(之前的途中有画到)
    • 因此,两个数据是联动的
    var obj = {name :'why',age : 18,hobby :['电影','阅读','音乐']};
    var o = obj;
    //改变对象中的简单数据类型观察是否有影响
    o.age = 12;
    console.log(obj.age,o.age);  //12
    //改变对象中的简单数据类型观察是否有影响
    o.hobby[1] = '吃嘛嘛香';
    console.log(obj.hobby);  // ["电影", "吃嘛嘛香", "音乐"]
    console.log(o.hobby);    // ["电影", "吃嘛嘛香", "音乐"]

在这里插入图片描述
两个对象共享同一份内存空间,不管修改简单数据类型数据还是复杂类型数据,都会对另一份数据产生影响.

  • 浅拷贝
    • 浅拷贝会创建一个新的对象,这个对象有这原始对象属性值的一份精确拷贝,但是属性值的拷贝是通过赋值来完成的.
    • 也就是存在我们之前讨论过的问题
      • 如果是基本数据类型,拷贝的就是基本数据类型的值,会有一份独立的存储空间
      • 如果是引用数据类型:拷贝的就是引用,引用虽然有两个,但是指向的内存空间依旧还是同一个.
    • 所以如果改变对象中的复杂数据类型,就会影响到另外一个对象
    • 因此仅仅是表面上得到了一个新的对象,其内部的复杂数据类型还是会存在共享同一个对象的情况
    • 所以我们把这种情况叫做浅拷贝

我们来看看下面浅拷贝的代码:

    var obj = {name :'why',age : 18,hobby :['电影','阅读','音乐']};
    var o = {};
    //浅拷贝一份obj
    for(let k in obj) {
      o[k] = obj[k];
    }
    console.log(o); //{name :'why',age : 18,hobby :['电影','阅读','音乐']};

    //改变对象中的简单数据类型观察是否有影响
    o.age = 12;
    console.log(obj.age,o.age);  // 18 12

    //改变复杂数据类型观察
    o.hobby[1] = '超级爱吃火锅';
    console.log(obj.hobby);  //["电影", "超级爱吃火锅", "音乐"]
    console.log(o.hobby);    //['电影','阅读','音乐']

通过上面的示例我们可以知道:

  • 基本数据类型的值通过赋值,在内存中存在两个备份,互不影响;
  • 复杂数据类型的值通过赋值,引用得到了两份,但是对象本身只有一份,改变其中一个,另一个也会改变(因为就是同一个对象,所以叫改变也不贴切)
  • 复杂数据类型的值通过浅拷贝,最外层对象拷贝得到了一个新的对象,但是内部的复杂数据类型的属性的对象还是原来对象引用指向的那一个,只拷贝了引用,所以修改复杂数据类型的属性hobby上的第一个元素,新旧对象都发生了改变。
    如下图:
    在这里插入图片描述
    这是因为浅拷贝只复制一层对象的属性,并不包括对象里面的为引用类型的数据。所以就会出现改变浅拷贝得到的 o中的引用类型时,会使原始数据得到改变。
类型和原数据是否指向同一对象第一层数据为基本数据类型原数据中包含子对象
赋值改变使原数据一同改变改变使原数据一同改变
浅拷贝改变不会使原数据一同改变改变使原数据一同改变
浅拷贝改变不会使原数据一同改变改变不会使原数据一同改变

深拷贝

到这里我们对赋值和浅拷贝都有了一定的了解,那么到底什么是深拷贝我们是不是有了那么一丢丢的感觉.

  • 深拷贝的概念:
    • 深拷贝会创建一个一模一样的对象
    • 新对象和原对象完全不共享堆内存
    • 修改新对象不会的数据不会改变原对象
    • 也就是说不管原对象有多么复杂,深拷贝会将其属性(属性下的属性)都赋值一份,不会出现像浅拷贝那样,复杂数据类型依旧在共享内存的状况.

通过下面的代码来实现深拷贝:

    var obj = {name :'why',age : 18,hobby :['电影','阅读','音乐']};
    var o = {};
    for(let k in obj) {
      let item = obj[k];
      if(item instanceof Array) {
        console.log(11)
        o[k] = [];
        for(index in item) {
          o[k][index] = item[index];
        }
      }else {
		 o[k] = item;
	  } 
    }
    console.log(o)  //{name :'why',age : 18,hobby :['电影','阅读','音乐']};


    //修改对象中的简单数据类型观察
    o.name = 'gh';
    console.log(obj.name, o.name);  //why gh

    //修改复杂数据类型观察
    o.hobby[1] = '看你变了没';
    console.log(obj.hobby);   // ["电影", "阅读", "音乐"]
    console.log(o.hobby);     //["电影", "看你变了没", "音乐"]
 

通过上面的代码我们可以发现:

  • 基本数据类型,在内存中存在两个备份,两个对象之间的数据互补影响
  • 复杂数据类型通过深拷贝引用得到了两份,对象本身也有两份,改变其中的一个,另一个并不会发生改变

如下图所示:
在这里插入图片描述
所以我们可以这样认为,浅拷贝只是肤浅的拷贝一个对象,而不拷贝对象的所有,新旧对象存在共享同一块堆内存的情况。但深拷贝会另外创造一个一模一样的对象,新对象跟原对象不共享堆内存,修改新对象不会改到原对象。

所以我们在这里给出神浅拷贝的定义:

浅拷贝 : 只拷贝一层,更深层次对象级别的只能拷贝引用
深拷贝: 拷贝多层,每一级别的数据都会拷贝

封装深拷贝函数

    //封装深拷贝函数
    function deepCopy(newObj, oldObj) {
      for (k in oldObj) {
        let item = oldObj[k];
        //判断旧对象的属性是不是数组
        if (item instanceof Array) {
          //初始化newObj对象对用的属性为数组
          newObj[k] = [];
          //再次调用深拷贝函数
          deepCopy(newObj[k], item)
          //判断旧对象的属性是不是对象
        } else if (item instanceof Object) {
          //初始化newObj对象对用的属性为对象
          newObj[k] = {};
          //再次调用深拷贝函数
          deepCopy(newObj[k], item)
          //否则就是普通类型数据,直接进行拷贝
        } else {
          newObj[k] = item;
        }
      }
    }

针对上面的封装,我们提出一个问题???

为什么要先判断是不是Array类型而不是先判断是不是Object类型.

    console.log(Array instanceof Object)  //true

因为数组判断是否为对象类型时也会为true,这是因为intanceof进行查找时也会查找原型链,所以为true.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值