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

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

知识扫盲


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

我们编写一段简单的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方法即可。


总结


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


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


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


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



相关文章
|
5月前
|
Java 编译器 C++
【Java基础面试一】、为什么Java代码可以实现一次编写、到处运行?
这篇文章解释了Java能够实现“一次编写,到处运行”的原因,主要归功于Java虚拟机(JVM),它能够在不同平台上将Java源代码编译成的字节码转换成对应平台的机器码,实现跨平台运行。
【Java基础面试一】、为什么Java代码可以实现一次编写、到处运行?
|
6天前
|
缓存 安全 Java
【JavaEE】——单例模式引起的多线程安全问题:“饿汉/懒汉”模式,及解决思路和方法(面试高频)
单例模式下,“饿汉模式”,“懒汉模式”,单例模式下引起的线程安全问题,解锁思路和解决方法
|
2月前
|
存储 Java 程序员
Java基础的灵魂——Object类方法详解(社招面试不踩坑)
本文介绍了Java中`Object`类的几个重要方法,包括`toString`、`equals`、`hashCode`、`finalize`、`clone`、`getClass`、`notify`和`wait`。这些方法是面试中的常考点,掌握它们有助于理解Java对象的行为和实现多线程编程。作者通过具体示例和应用场景,详细解析了每个方法的作用和重写技巧,帮助读者更好地应对面试和技术开发。
137 4
|
4月前
|
ARouter 测试技术 API
Android经典面试题之组件化原理、优缺点、实现方法?
本文介绍了组件化在Android开发中的应用,详细阐述了其原理、优缺点及实现方式,包括模块化、接口编程、依赖注入、路由机制等内容,并提供了具体代码示例。
52 2
|
5月前
|
Java
【Java基础面试二十】、介绍一下Object类中的方法
这篇文章介绍了Java中Object类的常用方法,包括`getClass()`、`equals()`、`hashCode()`、`toString()`、`wait()`、`notify()`、`notifyAll()`和`clone()`,并提到了不推荐使用的`finalize()`方法。
【Java基础面试二十】、介绍一下Object类中的方法
|
5月前
|
Java API 索引
【Java基础面试二十四】、String类有哪些方法?
这篇文章列举了Java中String类的常用方法,如`charAt()`、`substring()`、`split()`、`trim()`、`indexOf()`、`lastIndexOf()`、`startsWith()`、`endsWith()`、`toUpperCase()`、`toLowerCase()`、`replaceFirst()`和`replaceAll()`,并建议面试时展示对这些方法的熟悉度,同时深入理解部分方法的源码实现。
【Java基础面试二十四】、String类有哪些方法?
|
5月前
|
存储 缓存 Java
面试问Spring循环依赖?今天通过代码调试让你记住
该文章讨论了Spring框架中循环依赖的概念,并通过代码示例帮助读者理解这一概念。
面试问Spring循环依赖?今天通过代码调试让你记住
|
5月前
|
JavaScript
【Vue面试题九】、Vue中给对象添加新属性界面不刷新?
这篇文章讨论了Vue中给对象动态添加新属性时界面不刷新的问题,并提供了三种解决方案:使用`Vue.set()`方法来确保新属性是响应式的并触发视图更新,使用`Object.assign()`创建新对象以合并新属性,以及作为最后手段的`$forceUpdate()`进行强制刷新。文章还简要分析了Vue 2和Vue 3在数据响应式实现上的差异。
|
5月前
|
Java
【Java集合类面试三十】、BlockingQueue中有哪些方法,为什么这样设计?
BlockingQueue设计了四组不同行为方式的方法用于插入、移除和检查元素,以适应不同的业务场景,包括抛异常、返回特定值、阻塞等待和超时等待,以实现高效的线程间通信。
|
5月前
|
JavaScript
【Vue面试题八】、为什么data属性是一个函数而不是一个对象?
这篇文章解释了为什么在Vue中组件的`data`属性必须是一个函数而不是一个对象。原因在于组件可能会有多个实例,如果`data`是一个对象,那么这些实例将会共享同一个`data`对象,导致数据污染。而当`data`是一个函数时,每次创建组件实例都会返回一个新的`data`对象,从而确保了数据的隔离。文章通过示例和源码分析,展示了Vue初始化`data`的过程和组件选项合并的原理,最终得出结论:根实例的`data`可以是对象或函数,而组件实例的`data`必须为函数。
【Vue面试题八】、为什么data属性是一个函数而不是一个对象?

热门文章

最新文章