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

前言

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

相关推荐
GoAI2 小时前
DeepSeek Harness Memory (一)主流记忆插件总结
后端·算法·面试
卸任2 小时前
Vibe coding工具使用心得
前端·ai编程
brien93 小时前
为什么模型标称 1M,Google Antigravity 聊到 256K 却突然失忆?我们给它做了个原生行内监控圆环
前端
修炼的dance3 小时前
录屏关键帧间隔多少合适?六档实测,2秒到10秒小了75.6%
前端
Lank_M3 小时前
关键帧从1秒拉到10秒,录屏小87.3%,摄像头小10.8%
前端
补码缩写3 小时前
用 ffprobe 拆录屏,八成字节在 29 个关键帧上
前端
百思不得小李3 小时前
一个复杂的动态表单是如何实现的
前端·设计模式
陈随易3 小时前
Cmder 停更 17 个月,2.0 支持 Windows Terminal
前端·后端·程序员
清晨Coding4 小时前
删了 Cookie 还能自动登录?Google系网站登录态自动恢复的原理分析
javascript
风骏时光牛马4 小时前
前沿视界
前端