面试官:你有多少种方法实现对象深拷贝?手撕一下代码!

简介: 前言深拷贝问题是一道老生常谈的前端面试题了。为什么要实现深拷贝大家也一定明白,作为一个程序员,值类型和引用的类型的区别大部分人应该都是知道的。面试官问这道题的道理也很简单,就是考虑你的基础知识是否牢固。很多小伙伴可能只知道个概念,或者大概知道有哪些方法,总是云里雾里的感觉。今天我们就好好理一理如何实现深拷贝!

知识扫盲


为了照顾一些基础不太好的同学,这里简单举一个小例子,让大家明白为什么要实现对象的深拷贝?

我们编写一段简单的JS代码。

代码如下:

let userA = {
    name: "张三",
    age: 24
}
let userB = userA;
userB.age = 30;
console.log(userA);


输出结果:

1.png


上段代码中我们简单的将userA赋值给了userB,然后更改了userB的age,但是控制台打印userA的时候发现age也变了,这就是问题所在。


通过上面的列子可以简单的看出,对象不能通过简单的赋值,所以我们衍生出了浅拷贝和深拷贝,上面那段代码可以简单理解为浅拷贝,通常我们浅拷贝很容易出现问题,所以我们一般复制对象的时候都是用深拷贝,本篇文章着重讲深拷贝。


问题归根结底:

两个对象引用了同一个内存地址。


1. 利用JSON

这种实现深拷贝的方式非常的简单,利用的是JSON.parse()和JSON.stringify()两个方法。


原理介绍:

JSON.stringify将对象转成JSON字符串,再用JSON.parse把字符串解析成对象,在这转换过程中,会开辟新的内存空间,也就实现了深拷贝。


示例代码:

let userA = {
  name: "张三",
  age: 24
}
let userB = JSON.parse(JSON.stringify(userA));
userB.age = 30;
console.log(userA);


输出结果:2.png

可以看到此时我们的userA没有受到影响,说明两个对象独立开来了。


虽然这种方式非常的简单,但是它并没有那么完善,有一些情况它还是解决不了的,主要问题如下:

  • 忽略 undefinedsymbol
  • 无法拷贝function
  • 无法拷贝循环引用对象(比如对象属性的值就是它自己)
  • 对Date、RegExp等一些内置函数的处理会出现问题,比如时间不再是对象格式。



问题拷贝代码:

let userA = {
  name: "张三",
  age: 24,
  num: undefined,
  sex: () => {
    console.log("我是函数")
  },
  time: new Date()
}
let userB = JSON.parse(JSON.stringify(userA));
userB.age = 30;
console.log("对象userA", userA);
console.log("对象userB", userB);


输出结果:3.png

可以看到我们对象userB的function不见了,时间也变为了字符串格式,num字段也不见了,这就是问题所在。


2. 递归实现


这种方式也是大家比较常用的,也是面试中小伙伴回答得最多的,有些小伙伴可能背得滚瓜烂熟,但是有很大部分小伙伴只知道这个方法,让他手撕代码却写不出来。


递归实现的原理很简单,就是循环遍历我们的对象,如果遇到属性是对象的,再一次递归循环遍历即可。


示例代码:

let userA = {
  name: "张三",
  age: 20,
  sex: () => {
    console.log("小猪课堂");
  },
  arr: ["1", "231", "342242"],
  obj: {
    like: "打游戏"
  },
  likesFood: new Set(["fish", "banana"]),
  date: new Date()
};
function deepClone(obj) {
  // 如果传入的类型不对,则不做处理
  if (typeof obj !== "object" || obj === null) {
    return;
  }
  let newObj = {}; // 新对象
  const dataKeys = Object.keys(obj); // 获取原对象所有键值
  dataKeys.forEach((value) => {
    const currentValue = obj[value];
    // 基础类型直接赋值
    if (typeof currentValue !== "object" || currentValue === null) {
      newObj[value] = currentValue;
    } else if (Array.isArray(currentValue)) {
      // 实现数组的深拷贝
      newObj[value] = [...currentValue];
    } else if (currentValue instanceof Set) {
      // 实现set数据的深拷贝
      newObj[value] = new Set([...currentValue]);
    } else if (currentValue instanceof Map) {
      // 实现map数据的深拷贝
      newObj[value] = new Map([...currentValue]);
    } else if (currentValue instanceof Date) {
      // 日期类型深拷贝
      newObj[value] = new Date(currentValue.valueOf())
    } else {
      // 普通对象则递归赋值
      newObj[value] = deepClone(currentValue);
    }
  });
  return newObj;
}
let userB = deepClone(userA);
userB.obj.like = "睡觉"
console.log("userA",userA)
console.log("userB",userB)


输出效果:4.png


上面的代码将我们常见的类型实现了深拷贝,比如function、对象、数组、日期等等,基本上能够满足我们日常的开发,如果还有自己需要深拷贝的类型,大家直接再加判断即可。


问题所在:

虽然递归能够满足我们日常中大部分列子,很多小伙伴用的就是这种方法,但是它也有问题所在,比如循环引用的问题,对象的属性值就是该对象。

代码如下:

let userA = {
  name: "张三",
  age: 20,
  sex: () => {
    console.log("小猪课堂");
  },
  arr: ["1", "231", "342242"],
  obj: {
    like: "打游戏"
  },
  likesFood: new Set(["fish", "banana"]),
  date: new Date()
};
userA.name = userA;

执行结果:

5.png

可以看到我们就加了一行代码,将name的值改为了userA,这就产生了循环引用/此时我们在使用该递归进行深拷贝,直接报栈溢出了,进入了死循环!


3. 递归实现升级版


为了解决循环引用的问题,我们稍微改造一下我们的代码即可。


问题解决思路:

我们需要判断要拷贝的对象,是不是已经拷贝过,而不要循环拷贝,这里我们借助缓存的思想。

实现代码:

let userA = {
  name: "张三",
  age: 20,
  sex: () => {
    console.log("小猪课堂");
  },
  arr: ["1", "231", "342242"],
  obj: {
    like: "打游戏"
  },
  likesFood: new Set(["fish", "banana"]),
  date: new Date()
};
userA.name = userA;
function deepClone(obj, hashMap = new WeakMap()) {
  // 如果传入的类型不对,则不做处理
  if (typeof obj !== "object" || obj === null) {
    return;
  }
  // 查缓存字典中是否已有需要克隆的对象,有的话直接返回同一个对象(同一个引用,不用递归无限创建进而导致栈溢出了)
  if (hashMap.has(obj)) {
    return hashMap.get(obj);
  }
  let newObj = {}; // 新对象
  const dataKeys = Object.keys(obj); // 获取原对象所有键值
  dataKeys.forEach((value) => {
    const currentValue = obj[value];
    // 基础类型直接赋值
    if (typeof currentValue !== "object" || currentValue === null) {
      newObj[value] = currentValue;
    } else if (Array.isArray(currentValue)) {
      // 实现数组的深拷贝
      newObj[value] = [...currentValue];
    } else if (currentValue instanceof Set) {
      // 实现set数据的深拷贝
      newObj[value] = new Set([...currentValue]);
    } else if (currentValue instanceof Map) {
      // 实现map数据的深拷贝
      newObj[value] = new Map([...currentValue]);
    } else if (currentValue instanceof Date) {
      // 日期类型深拷贝
      newObj[value] = new Date(currentValue.valueOf())
    } else {
      hashMap.set(obj, newObj); // 哈希表缓存新值
      // 普通对象则递归赋值
      newObj[value] = deepClone(currentValue,hashMap);
    }
  });
  return newObj;
}
let userB = deepClone(userA);
userB.obj.like = "睡觉"
console.log("userA", userA)
console.log("userB", userB)


实现效果:6.png

上段代码解决了我们循环引用的问题,主要利用了WeakMap()这个方法,它的作用其实主要就是创建一个缓存表,我们深拷贝时,会先判断该属性值是否在缓存表当中,存在则说明是循环引用。

想入深入了解Weakmap的小伙伴可以参考官网,我们这里只实现方法:MDN



4. Object.assign()


Object.assign()是一种比较简单的深拷贝方法,其实说它是深拷贝,一点都不严谨,因为它只会拷贝第一层,也就是当对象内嵌套有对象的时候,被嵌套的对象进行的还是浅拷贝。


示例代码:

let userA = {
  name: "张三",
  age: 24
}
let userB = Object.assign({},userA);
userB.age = 30;
console.log("userA",userA);
console.log("userB",userB);

输出结果:

7.png

上段代码实现了简单的深拷贝,在一般的开发过程中,我们还是可以使用的,使用起来还是比较简单,详细用法可以参考官网:MDN


问题所在:


无法进行深层次的拷贝,只能进行对象的第一层深拷贝。


5. 利用Lodash库(完美)


这个库相信很多小伙伴都用过,简直是前端开发利器。lodash是一套工具库,内部封装了很多字符串、数组、对象等常见数据类型的处理函数。


它提供了给我们进行深拷贝的方法,各种情况他都考虑到了,如果不在乎引入第三方库的话,强烈建议使用此方法。


示例代码:

_.cloneDeep(obj1);

直接调用提供给我们的_.cloneDeep方法即可。

官方中文文档:官方文档


6. 利用Jquery


Jquery提供了深拷贝的方法给我们,但是目前这个大前端环境下,很多人不在使用jquery,所以这儿就简单介绍一下。


示例代码:

$.extend(true,{},obj)
复制代码

直接调用extend方法即可。


总结


这里只举例了常用的一些方法,还有很多奇奇怪怪的方法这里就不过多说了。但是深拷贝的原理始终都是不变的,我们需要解决的问题就是开辟一块新的内存空间,只要朝着这个思路下去,方法总是有很多。


欢迎关注公众号:资料分享大师(专注分享)


哔哩哔哩:小猪课堂(视频教程)


知乎:会飞的猪(作者动态)



相关文章
|
1月前
|
编译器 C++ Python
【C/C++ 泡沫精选面试题02】深拷贝和浅拷贝之间的区别?
【C/C++ 泡沫精选面试题02】深拷贝和浅拷贝之间的区别?
32 1
|
3月前
|
存储 JavaScript 前端开发
【面试题】JS的14种去重方法,看看你知道多少(包含数组对象去重)
【面试题】JS的14种去重方法,看看你知道多少(包含数组对象去重)
|
3月前
|
前端开发
【面试题】如何使用ES6 ... 让代码优雅一点?
【面试题】如何使用ES6 ... 让代码优雅一点?
|
3月前
|
存储 前端开发 JavaScript
【面试题】你是如何让js 代码变得简洁的?
【面试题】你是如何让js 代码变得简洁的?
|
3月前
|
JavaScript 前端开发 Java
【面试题】new 一个对象时,js 做了什么?
【面试题】new 一个对象时,js 做了什么?
|
3月前
|
JSON 前端开发 JavaScript
【面试题】JavaScript 深拷贝和浅拷贝 高级
【面试题】JavaScript 深拷贝和浅拷贝 高级
|
3月前
|
JSON JavaScript 前端开发
【面试题】马上金九银十了,简历该准备起来了,面试题你准备好了吗 ?浅谈 JS 浅拷贝和深拷贝
【面试题】马上金九银十了,简历该准备起来了,面试题你准备好了吗 ?浅谈 JS 浅拷贝和深拷贝
|
2月前
|
存储 编译器 程序员
近4w字吐血整理!只要你认真看完【C++编程核心知识】分分钟吊打面试官(包含:内存、函数、引用、类与对象、文件操作)
近4w字吐血整理!只要你认真看完【C++编程核心知识】分分钟吊打面试官(包含:内存、函数、引用、类与对象、文件操作)
107 0
|
3月前
|
存储 Java Apache
【面试问题】深拷贝和浅拷贝的区别?
【1月更文挑战第27天】【面试问题】深拷贝和浅拷贝的区别?
|
3月前
|
安全 Java 程序员
面试题:什么是对象安全?
面试题:什么是对象安全?
12 0