面试官:请手写一个深拷贝函数

前言

我们都知道,JavaScript是一门弱类型脚本语言,在处理数据复制时涉及到深拷贝和浅拷贝两种不同的概念。这两种拷贝方式在处理对象和数组时有着重要的区别,深拷贝和浅拷贝的选择对于程序的正确性和性能有着深远的影响。下面我将讲解一下深浅拷贝,并尝试着去手写实现一个深拷贝函数。

下面是一道面试题,在我们学习完深浅拷贝知识之后我们再倒过来做一遍。当然,如果你已经会了,显然这篇文章对你的帮助也不会很大。

题目: 实现一个深拷贝函数 deepClone,该函数能够对传入的对象进行深度复制,确保原始对象与拷贝对象之间没有引用关系。

要求:

  1. 深拷贝应该适用于各种数据类型,包括对象、数组、字符串、数字等。
  2. 考虑处理循环引用的情况,避免陷入无限递归。
  3. 你可以选择使用任何合适的方式来实现深拷贝,例如递归、JSON.parse 和 JSON.stringify,或其他方法。

什么是浅拷贝?

浅拷贝是一种复制对象或数组的方法,但它只复制了对象的第一层,而不会递归复制嵌套在其中的对象或数组。简而言之,浅拷贝创建了一个新的对象,但该对象的嵌套结构仍然与原始对象共享引用。对拷贝后的第二层对象的值进行修改,会影响到被拷贝的对象的值。

下面介绍一下JS中一些常见的浅拷贝的方法:

1. Object.create(x)

  • Object.create() 方法创建一个新对象,该对象的原型链继承自指定的对象 x
js 复制代码
let obj = { name: '小明', details:{ age: 25 } };
let objCopy = Object.create(obj);

objCopy.name = '小红';
objCopy.details.age = 30;

console.log(obj.name); // 输出: 小明,原对象未受影响
console.log(obj.details.age); // 输出: 30,原对象受影响

2.Object.assign({},x)

  • Object.assign() 方法用于将所有可枚举属性的值从一个或多个源对象复制到目标对象 {}
js 复制代码
let obj = { name: '小明', details:{ age: 25 } };
let objCopy = Object.assign({},obj);

objCopy.name = '小红';
objCopy.details.age = 30;

console.log(obj.name); // 输出: 小明,原对象未受影响
console.log(obj.details.age); // 输出: 30,原对象受影响

数组的浅拷贝

1.concat

  • concat 方法用于合并两个或多个数组。它创建一个新的数组,其中包含被调用的数组的浅拷贝。
js 复制代码
let arr = [1, 2, 3, { value: 4 }];
let arrCopy = arr.concat();

arrCopy[2] = 5;

console.log(arr); // 输出: [1, 2, 3, { value: 4 }],原数组未受影响
arrCopy[3].value = 10;

console.log(arr); // 输出: [1, 2, 3, { value: 10 }],原数组受影响

2.slice

  • slice返回一个新数组,其中包含被调用的数组的浅拷贝。
js 复制代码
let arr = [1, 2, 3, { value: 4 }];
let arrCopy = arr.slice();

arrCopy[2] = 5;

console.log(arr); // 输出: [1, 2, 3, { value: 4 }],原数组未受影响
arrCopy[3].value = 10;

console.log(arr); // 输出: [1, 2, 3, { value: 10 }],原数组受影响

3.数组解构

  • 使用数组解构赋值也能进行浅拷贝
js 复制代码
let arr = [1, 2, 3, { value: 4 }];
let [...arrCopy] = arr;

arrCopy[2] = 5;

console.log(arr); // 输出: [1, 2, 3, { value: 4 }],原数组未受影响
arrCopy[3].value = 10;

console.log(arr); // 输出: [1, 2, 3, { value: 10 }],原数组受影响

什么是深拷贝?

  • 递归创建全新对象,对拷贝对象的值进行修改不会影响到原始对象的值。

深拷贝是一种创建对象或数组完全独立副本的方式,包括嵌套在其中的对象或数组。深拷贝确保原始对象和拷贝对象之间不存在引用关系,避免了副作用。

常见的深拷贝方法

JOSN.parse(JSON.stringify(obj))

js 复制代码
let obj = { name: '小明', value: { a: 1, b: 2 } };
let deepCopy = JSON.parse(JSON.stringify(obj));

deepCopy.name = '小红';  // 修改第一层内容

console.log(obj);  // 输出:{ name: '小明', value: { a: 1, b: 2 } } 原始对象obj的值没有受影响

deepCopy.value.a =  3;  // 修改第二层内容

console.log(obj);  // 输出:{ name: '小明', value: { a: 1, b: 2 } } 原始对象obj的值没有受影响

上述方法存在以下缺点:

1.无法拷贝 undefined、function、Symbol、bigint:

  • JSON.stringifyJSON.parse 在序列化和反序列化时会丢弃 undefined、function、Symbol,以及不能正确处理 bigint 类型。这意味着深拷贝后的对象可能丧失这些类型的信息。

例子:

js 复制代码
let obj = { a: undefined, b: function() {}, c: Symbol('symbol'), d: BigInt(123) };
let deepCopy = JSON.parse(JSON.stringify(obj));
console.log(deepCopy); // 输出: { a: null, b: null, c: null, d: 123 }

2.无法处理循环引用:

  • 如果对象存在循环引用(即对象的属性之间形成一个闭环),JSON.stringify 会抛出异常,因为 JSON 格式无法表示循环引用。

例子:

js 复制代码
let obj = { a: 1 };
obj.self = obj; // 形成循环引用
// 以下代码会抛出异常
// let deepCopy = JSON.parse(JSON.stringify(obj));

手写一个完美的深拷贝

经过上面的分析之后,我们都知道要实现一个完美的深拷贝就必须解决上述常用方法-JOSN.parse(JSON.stringify(obj))带来的问题。

js 复制代码
function deepClone(obj, clonedObjects = new WeakMap()) {
  if (obj === null || typeof obj !== 'object') {
    return obj;
  }

  // 检查是否已经拷贝过该对象,避免循环引用导致无限递归
  if (clonedObjects.has(obj)) {
    return clonedObjects.get(obj);
  }

  let clone;

  // 处理数组
  if (Array.isArray(obj)) {
    clone = [];
    clonedObjects.set(obj, clone);
    for (let i = 0; i < obj.length; i++) {
      clone[i] = deepClone(obj[i], clonedObjects);
    }
  }
  // 处理对象
  else if (obj instanceof Object) {
    clone = {};
    clonedObjects.set(obj, clone);
    for (let key in obj) {
      if (obj.hasOwnProperty(key)) {
        clone[key] = deepClone(obj[key], clonedObjects);
      }
    }
  }

  return clone;
}

上述方法是如何解决JOSN.parse(JSON.stringify(obj))带来的问题的呢?这里我们得感谢WeakMap()。

1.使用WeakMap处理循环引用

  • WeakMap 是一个弱映射表,它允许你在没有内存泄漏风险的情况下将对象作为键存储在其中。在 deepClone 中,clonedObjects 是一个 WeakMap,用于跟踪已经拷贝过的对象。
为什么不使用Map,而是WeakMap?

使用 Map 替代 WeakMap 会导致潜在的内存泄漏问题,因为 Map 对键的引用是强引用。而 WeakMap 使用的是弱引用,可以避免因为该引用而阻止垃圾回收器对对象的回收。 在深拷贝的场景中,当你使用 Map 时,可能会导致整个拷贝的对象及其子对象都无法被垃圾回收,因为 Map 对键的引用是强引用。而使用 WeakMap 时,当不再存在对原对象的引用时,对应的键值对就可以被垃圾回收,不会造成内存泄漏。

2.对于 undefined、function、Symbol、bigint 的处理:

js 复制代码
if (obj === null || typeof obj !== 'object') {
  return obj;
}

解释:

在进入递归部分之前,首先进行了基础的判断,如果 obj 是 null 或者不是对象类型,就直接返回 obj。这样,对于 undefined、function、Symbol、bigint 这些特殊类型,就会直接复制。

检验是否解决了

1.循环引用

2.对于 undefined、function、Symbol、bigint 的处理

相关推荐
zqx_715 分钟前
随记 前端框架React的初步认识
前端·react.js·前端框架
惜.己32 分钟前
javaScript基础(8个案例+代码+效果图)
开发语言·前端·javascript·vscode·css3·html5
什么鬼昵称1 小时前
Pikachu-csrf-CSRF(get)
前端·csrf
长天一色1 小时前
【ECMAScript 从入门到进阶教程】第三部分:高级主题(高级函数与范式,元编程,正则表达式,性能优化)
服务器·开发语言·前端·javascript·性能优化·ecmascript
NiNg_1_2342 小时前
npm、yarn、pnpm之间的区别
前端·npm·node.js
秋殇与星河2 小时前
CSS总结
前端·css
NiNg_1_2342 小时前
Vue3 Pinia持久化存储
开发语言·javascript·ecmascript
读心悦2 小时前
如何在 Axios 中封装事件中心EventEmitter
javascript·http
BigYe程普2 小时前
我开发了一个出海全栈SaaS工具,还写了一套全栈开发教程
开发语言·前端·chrome·chatgpt·reactjs·个人开发
神之王楠2 小时前
如何通过js加载css和html
javascript·css·html