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

前言

我们都知道,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 的处理

相关推荐
程序员爱技术1 小时前
Vue 2 + JavaScript + vue-count-to 集成案例
前端·javascript·vue.js
并不会2 小时前
常见 CSS 选择器用法
前端·css·学习·html·前端开发·css选择器
悦涵仙子2 小时前
CSS中的变量应用——:root,Sass变量,JavaScript中使用Sass变量
javascript·css·sass
衣乌安、2 小时前
【CSS】居中样式
前端·css·css3
兔老大的胡萝卜2 小时前
ppk谈JavaScript,悟透JavaScript,精通CSS高级Web,JavaScript DOM编程艺术,高性能JavaScript pdf
前端·javascript
低代码布道师2 小时前
CSS的三个重点
前端·css
耶啵奶膘4 小时前
uniapp-是否删除
linux·前端·uni-app
王哈哈^_^5 小时前
【数据集】【YOLO】【目标检测】交通事故识别数据集 8939 张,YOLO道路事故目标检测实战训练教程!
前端·人工智能·深度学习·yolo·目标检测·计算机视觉·pyqt
cs_dn_Jie6 小时前
钉钉 H5 微应用 手机端调试
前端·javascript·vue.js·vue·钉钉
开心工作室_kaic6 小时前
ssm068海鲜自助餐厅系统+vue(论文+源码)_kaic
前端·javascript·vue.js