一文教你区分赋值、浅拷贝和深拷贝,手撕浅拷贝与深拷贝函数

浅拷贝与深拷贝

前言

通过这篇文章你可以了解赋值、浅拷贝和深拷贝的区别,同时你可以手写一个属于自己的浅拷贝与深拷贝函数


本篇文章是博主看书,看文章,加代码实践总结的,如果有错误或者歧义的地方希望大佬们指出,免得误导更多人!!!

如果觉得有用,可以收藏下次再看!!

最后,花了大半天整理总结的,希望大家能给个点赞鼓励一下~


正文

在了解赋值、浅拷贝和深拷贝的区别前,首先要知道js中一共有两种数据类型:

  • 简单数据类型:Number、Boolean、String、Undefined、Null和Symbol
  • 复杂数据类型:Object、Array等

它们在内存中的存储形式是不一样的!!

简单数据类型又叫做基本数据类型或者值类型,存放的是值且存放在栈里面

复杂类型又叫做引用类型,在存储时变量中存储的仅仅是地址(引用)。复杂数据类型,在栈里面存放地址,这个地址指向堆里面的数据

不理解?那我们来举个例子。当我们写下如下代码时,我们在内存中存储了一个简单数据类型的num,和一个复杂数据类型的obj1

let obj1 = {
    name: 'joney',
    school: {
        address:"湖南"
    }
}
let num=2;

这时,num的值2直接存储在栈里。obj1在栈中存的是地址(0x80804513),地址指向的是堆里面的数据,即obj2实际的内容存储在堆里
在这里插入图片描述

赋值

下面是基本数据类型的赋值

let num=2;
let n=num;
n=222;
console.log('n',n);
console.log('num',num);

可见:当n发生变化时,num并不发生变化,所以num和n在栈中地址并不相同。即在简单数据类型发生赋值时,系统会在栈中为新变量分配一个地址空间,所以两个变量是完全独立的,不受影响。
在这里插入图片描述
在这里插入图片描述


接着,我们再来看复杂数据类型的赋值

let obj1 = {
    name: 'joney',
    school: {
        address:"湖南"
    }
}
let obj2 = obj1


obj2.name = 'woody'
obj2.school.address='上海'
console.log('obj1',obj1);
console.log('obj2',obj2);  

可以看出,当obj2的内存发生变化时,obj1的内存也会随着变化,也就是说obj1和obj2在栈中保存的内存地址是一样的,它们指向堆内存中相同的位置
在这里插入图片描述
不明白? 来看这张图!!我们说复杂数据类型在栈里面存放地址,这个地址指向堆里面的数据。所以obj1和obj2实际上公用的一个数据,任何一个对象的改动都会影响另外一个对象
在这里插入图片描述

浅拷贝

浅拷贝是按位拷贝对象,它会创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。

  • 如果属性是基本类型,拷贝的就是基本类型的值
  • 如果属性是内存地址(引用类型),拷贝的就是内存地址 ,因此如果其中一个对象改变了这个地址,就会影响到另一个对象。

换句话说,浅拷贝只会拷贝第一层属性,如果第一层不是基本数据类型,那么就只会拷贝引用(地址)!!!

如果还不理解,那我们来段代码试试。

在MDN文档中说到,slice()方法返回一个新的数组对象,这一对象是一个由 begin 和 end 决定的原数组的浅拷贝(包括 begin,不包括end)。

我们使用这个函数实现一个浅拷贝

let joney=['joney',{school:'湖南大学'}]
let woody =joney.slice()

joney[0]='Amy'
joney[1].school='hnu'
console.log('joney',joney);
console.log('woody',woody);

从结果中我们可以发现:浅拷贝只会拷贝第一层属性

  • 对于简单数据类型,浅拷贝得到的。所以joney[0]='Amy'的时候,woody[0]'不会变成Amy',因为两者是独立的空间,不会相互影响的。
  • 对于复杂数据类型,浅拷贝得到的是引用地址。所以joney[1].school='hnu'发生变化时,woody的内容也会变化。
    在这里插入图片描述

此外,我们还可以使用Object.assign...扩展运算符实现对象的浅拷贝。它同样只是在根属性(对象的第一层级)创建了一个新的对象,但是如果属性的值是对象的话只会拷贝一份相同的内存地址

所以当我们拷贝的是复杂数据类型时,需要注意:

当内存销毁的时候,指向这个内存空间的所有指针需要重新定义,不然会造成野指针错误。

野指针:就是指针指向的位置是不可知的指针。


现在来到我们的重点,如何手写一个浅拷贝呢? 如果上面的说明你听懂了,那其实很简单~

function shallowClone(obj) {
   let objClone = {}
   for (let i in obj) {
       objClone[i] = obj[i]
   }
   return objClone
 }

我们来测试一下

let joney=['joney',{school:'湖南大学'}]
let woody = shallowClone(joney)
// let woody =joney.slice()

joney[0]='Amy'
joney[1].school='湖南hun'
console.log('joney',joney);
console.log('woody',woody);

确定过眼神,是浅拷贝的特点~
在这里插入图片描述

深拷贝

相信理解了浅拷贝,你应该可以猜测到什么是深拷贝了吧

当对象和它所引用的对象一起拷贝时即发生深拷贝。深拷贝在拷贝的时候,需要将当前要拷贝的对象内的所有引用类型的属性进行完整的拷贝,也就是说拷贝出来的对象和原对象之间没有任何数据是共享的,所有的东西都是自己独占的一份

那如何实现深拷贝呢?使用JSON.parse(JSON.stringify())

  • JSON.stringify()把一个对象序列化成为一个JSON字符串.
  • JSON.parse()反序列化将JSON字符串变成一个新的对象.
let joney=['joney',{school:'湖南大学'}]
let woody =JSON.parse(JSON.stringify(joney))
joney[0]='Amy'
joney[1].school='湖南hun'
console.log('joney',joney);
console.log('woody',woody);

可以看到不管joney对象怎么变,woody对象都不会发生改变,这就是深拷贝的特点,它们
在这里插入图片描述


那如何实现一个深拷贝呢? 我们通过递归实现:不断遍历对象、数组直到里边都是基本数据类型,然后再去复制。

//定义检测数据类型的功能函数
function isObject(obj) {
    // null 的数据类型也是object
    return typeof obj === 'object' && obj != null;
}

function deepClone(source) {
    if (!isObject(source)) return source; // 非对象返回自身
    var target = Array.isArray(source) ? [] : {};
    for(var key in source) {
            if (isObject(source[key])) {
                target[key] = deepClone(source[key]); // 递归
            } else {
                target[key] = source[key];
        }
    }
    return target;
}
}

检验一下正确性:

let joney=['joney',{school:'湖南大学'},['1','2']]
// let woody =JSON.parse(JSON.stringify(joney))
woody=deepClone(joney)
joney[0]='Amy'
joney[1].school='湖南hun~~'
joney[2][1]='22'
console.log('joney',joney);
console.log('woody',woody);

在这里插入图片描述
注意:该方案存在问题,如果遇到循环引用,会陷入一个循环的递归过程,从而导致爆栈.

循环引用:当对象 1 中的某个属性指向对象 2,对象 2 中的某个属性指向对象 1 就会出现循环引用

function circularReference() {
  let obj1 = {};
  let obj2 = {
    b: obj1
  };
  obj1.a = obj2;
}

如何解决? 把拷贝过的值存起来,在拷贝的时候先判断是否已经拷贝过,如果有就直接取出来返回,没有再执行deepClone不就可以。

function deepClone(source, m = new Map()) {
    if (isObject(source)) {
      if (m.has(source)) return m.get(source)
      var target = Array.isArray(source) ? [] : {}
      m.set(source, target)
      const keys = Object.keys(source)
      for(let i = 0; i < keys.length; i++) {
        target[keys[i]] = deepClone(source[keys[i]], m)
      }
      return target
    }
    return source
  }


此外,还可以引用第三方库的lodash_.cloneDeep()。其解决了循环引用的问题:就是用一个栈记录所有被拷贝的引用值,如果再次碰到同样的引用值的时候,不会再去拷贝一遍,而是利用之前已经拷贝好的

var _ = require('lodash');
var obj1 = {
    a: 1,
    b: { f: { g: 1 } },
    c: [1, 2, 3]
};
var obj2 = _.cloneDeep(obj1);
console.log(obj1.b.f === obj2.b.f);// false

本篇文章到此结束,如有收获,记得点赞哦~

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

焦妮敲代码

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

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

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

打赏作者

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

抵扣说明:

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

余额充值