JS手写实现深拷贝(考虑循环引用)

170 篇文章 3 订阅
41 篇文章 2 订阅

作为一个前端,一定对深拷贝这个名词不陌生,深拷贝是非常重要的。

为什么存在深拷贝?

深拷贝这个名词的诞生也是有由来的,在JS里,所有的变量类型都可以简单分为基本类型和应用类型。

比如 123, 'aaa' 数值和字符串,属于基本类型,它们在进行赋值的时候,可以直接赋值,比如let a = 123, b = a;这个是没有问题的,ab互不影响。

但是[1, 2, 3] { a: 1 },像数组和对象这种引用类型,在进行赋值拷贝的时候,简单的=并不足够,如果你依然像对待基本类型一样对待引用类型的拷贝。你拷贝的只是一个单纯的内存地址,他们会互相影响。

基本类型和引用类型的关系,大家可以自己找资料学习,这里不过多赘述,反正要知道一点,深拷贝诞生是因为JS存在引用类型的数据导致的。

在前端的面试中,手写深拷贝的问题出现率非常之高。在一些大厂中,还要让你考虑很多的情况,比如循环引用之类的。

什么是循环引用?

比如

let obj1 = {};
let obj2 = {};
obj1.obj2 = obj2;
obj2.obj1 = obj1;

这样就会造成对象间的循环引用,这个也需要考虑在深拷贝的实现之中。

在这里插入图片描述

手写实现深拷贝(不考虑循环引用)

我们先实现一个最基本的深拷贝版本,不考虑循环应用。

function deepClone(obj, newObj) {
  newObj = newObj || {};

  for (let i in obj) {
    if (typeof obj[i] === "object") {
      newObj[i] =
        Object.prototype.toString.call(obj[i]) === "[object Array]" ? [] : {};
      deepClone(obj[i], newObj[i]);
    } else {
      newObj[i] = obj[i];
    }
  }
}

最基本的思路是依赖递归来实现。

对需要深拷贝的obj变量进行for in循环,判断每个属性的类型。我这里使用的是Object.prototype.toString方法来判断变量类型,也可以用其它方法。

如果当前循环的属性是基本类型,直接进行 = 赋值,如果是引用类型,就进行递归,直到当前属性递归到基本类型,再进行赋值。

其实还是比较简单的。

我们来验证一下,是否实现了深拷贝。

let obj = {
  name: "微信公众号: Code程序人生",
  age: 22,
  sex: "男",
  hobby: ["跑步", "读书", "睡觉"],
  fn: function () {
    console.log(this.name);
  },
};

let newObj = {};

deepClone(obj, newObj);

obj.name = 'CreatorRay1';
obj.hobby[0] = '运动';

console.log('newObj', newObj);
console.log('obj', obj);

在这里插入图片描述
通过结果可以看到,新创建的newObj对象跟obj对象互不联系,在修改某一方的值时,另一方并不受到影响,所以它们的内存地址是不一样的。

如果我们直接 = 赋值,会是什么样子?

let obj = {
  name: "微信公众号: Code程序人生",
  age: 22,
  sex: "男",
  hobby: ["跑步", "读书", "睡觉"],
  fn: function () {
    console.log(this.name);
  },
};

let newObj = {};

// deepClone(obj, newObj);

newObj = obj;

obj.name = 'CreatorRay1';
obj.hobby[0] = '运动';

console.log('newObj', newObj);
console.log('obj', obj);

在这里插入图片描述

两个对象相互影响。

那么,如果我们在这个版本进行循环引用呢?

let obj = {
  name: "微信公众号: Code程序人生",
  age: 22,
  sex: "男",
  hobby: ["跑步", "读书", "睡觉"],
  fn: function () {
    console.log(this.name);
  },
};

let obj1 = {};

let newObj = {};

obj.obj1 = obj1;
obj1.obj = obj;

deepClone(obj, newObj);

obj.name = 'CreatorRay1';
obj.hobby[0] = '运动';

console.log('newObj', newObj);
console.log('obj', obj);

在这里插入图片描述

会直接报错。

考虑循环引用实现深拷贝

在实现兼容循环引用之前,需要铺垫一下WeakMap的知识,实现的主要原理就是基于WeakMap的。

WeakMap可以算是Map的一个加强版。

同样都是key , value的形式保存变量,WeakMapMap最大的区别就是,WeakMap是弱引用的,当它的键在外部失去引用时,这组键值对自动删除。

基于WeakMap的原理,我们就可以全面完善深拷贝的代码。

function deepClones(origin, map = new WeakMap()) {
  // origin == undefined可以同时判断undefined和null
  if (origin == undefined || typeof origin !== "object") {
    return origin;
  } else if (origin instanceof Date) {
    return new Date(origin);
  } else if (origin instanceof RegExp) {
    return new RegExp(origin);
  } else {
    const key = map.get(origin);

    if (key) {
      return key;
    }

    const target = new origin.constructor();
    map.set(origin, target);
    for (let k in origin) {
      target[k] = deepClones(origin[k], map);
    }
    return target;
  }
}

针对DateRegExp等类型的变量,也可以额外进行判断。

解释一下这个版本和上个版本的区别:

  • origin == undefined可以同时判断undefinednull两种情况
  • target不是通过Object.prototype.toString来判断当前的属性是数组还是对象了,而是new当前属性的constructor,对于constructor不太了解的同学可以自行补习一下。简单解释就是如果当前属性是数组,它的constructor就是Array就相当于new Array了,属性是对象的话,同理new Object了。
  • 通过WeakMap来存储每次递归的键值对,如果当前递归的属性已存在就直接返回。

这个版本和上个版本相对,整体都清晰明了了,而且更加体现了自己的技术含量,如果你能在面试时写出这个版本的深拷贝,相信一定会给你加很多分。

下面我们来检验一下效果。

首先是正常拷贝。

let obj = {
  name: "微信公众号: Code程序人生",
  age: 22,
  sex: "男",
  hobby: ["跑步", "读书", "睡觉"],
  fn: function () {
    console.log(this.name);
  },
};

let obj1 = {};

// obj.obj1 = obj1;
// obj1.obj = obj;

let newObj = deepClones(obj);

obj.name = 'CreatorRay1';
obj.hobby[0] = '运动';

console.log('newObj', newObj);
console.log('obj', obj);

我们在使用中有些许的变化,上个版本我们传入了第二个参数,代表我们拷贝后的结果。这个版本就不需要额外传第二个参数了。
在这里插入图片描述
正常,两个对象未受到彼此影响。

试一下循环应用的情况。

let obj = {
  name: "微信公众号: Code程序人生",
  age: 22,
  sex: "男",
  hobby: ["跑步", "读书", "睡觉"],
  fn: function () {
    console.log(this.name);
  },
};

let obj1 = {};

obj.obj1 = obj1;
obj1.obj = obj;

let newObj = deepClones(obj);

obj.name = 'CreatorRay1';
obj.hobby[0] = '运动';

console.log('newObj', newObj);
console.log('obj', obj);

在这里插入图片描述
可以看到没有报错,而且循环引用的值也都正常显示。

欢迎大家关注我的公众号,有很多关于前端的内容哦
QQ:505417246
WX:18331092918
公众号:Code程序人生
B站账号:LuckyRay123
个人博客:http://rayblog.ltd/

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

CreatorRay

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

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

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

打赏作者

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

抵扣说明:

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

余额充值