面试官:实现过深拷贝吗?实现中你考虑了哪些问题?

我记得在一次面试中,面试官向我提出了深拷贝的问题。起初,我列举了几种深拷贝的方法,包括使用lodash库、JSON.stringify(JSON.parse(obj))以及原生API structuredClone()

随后,面试官继续追问我是否手写过深拷贝函数,我回答说确实有编写过。然后面试官进一步探询,问我在实现深拷贝时考虑了哪些问题。我的回答只包括了初始化一个空数组来处理数组类型,但我未能提到其他关键问题,如引用循环、函数和非枚举属性的考虑。

最终,因为我考虑问题不全面,我失去了这个工作机会。

1、简易版深拷贝

js 复制代码
// 判断是否是对象的工具函数
function isObject(val) {
  const type = typeof val;
  return (type === "object" || type === "function") && type !== "null";
}

// 简易深拷贝
function deepClone(obj) {
  // 简单数据类型直接返回
  if (!isObject(obj)) {
    return obj;
  }

  let newObject = {};
  // 如果是数组类型就初始化[]
  if (obj instanceof Array) {
    newObject = [];
  }
  for (const key in obj) {
    newObject[key] = deepClone(obj[key]);
  }
  return newObject;
}

2、特殊类型处理

Function

问题:由于函数是用来调用的,没有做深拷贝的必要,对函数做深拷贝反而会浪费内存空间。

解决方法:和简单数据类型做一样的处理即可。

js 复制代码
  if (typeof obj === 'function') {
    return obj
  }

Set

问题:我们都知道set类型是不可枚举的,所以我们无法被for...in...遍历。

解决方法:可以通过for...of...的方式进行遍历。

js 复制代码
  if (obj instanceof Set) {
    const newSet = new Set();
    for (const item of obj) {
      newSet.add(item);
    }
    return newSet;
  }

Symbol

问题:symbol类型可以选择不拷贝,因为 Symbol 是一种原始数据类型,每个 Symbol 都是唯一的,不具备可比较性。通常情况下,深拷贝的目的是复制对象的结构和值,而 Symbol 的唯一性和不可比较性使得它在深拷贝时可能不是一个有意义的操作,但是如果仍然希望在深拷贝时处理 Symbol 类型。

解决方法:可以重新创建一个symbol

js 复制代码
  if (typeof obj === "symbol") {
    const newSymbol = Symbol(obj.description);
    return newSymbol;
  }

当Symbol作为key时

为什么我们要考虑symbol作为key时的情况?下面举个例子

问题

js 复制代码
const symbol1 = Symbol(1);
const symbol2 = Symbol(2);
const obj = {
  name: "symbol",
  [symbol1]: "symbol1",
  [symbol2]: "symbol2",
};

for (const key in obj) {
  console.log(key); // 只会输出name
}

我们会发现当Symbol类型作为key时被忽略了,这是因为Symbol属性默认是不可枚举的

解决方法 :可以使用 Object.getOwnPropertySymbols()获取,单独遍历Symbol

Object.getOwnPropertySymbols()返回一个包含给定对象所有自有 Symbol 属性的数组。 如[ Symbol(1), Symbol(2) ]

js 复制代码
  const symbolKeys = Object.getOwnPropertySymbols(obj);
  for (const symbolKey of symbolKeys) {
    newObject[Symbol(symbolKey.description)] = deepClone(obj[symbolKey], map);
  }

3、循环引用处理

问题::循环引用是指一个对象包含对自身或包含对其他对象的引用链,形成一个循环,例如:

js 复制代码
const obj = {};
obj.a = obj;

如果没有处理循环引用,深拷贝可能会陷入无限循环,因为它会不断地尝试复制对象的属性,其中一个属性是对自身的引用,这将导致无限递归的拷贝过程。这不仅浪费了计算资源,还可能导致程序崩溃。

解决方法 :在深拷贝过程中使用一个数据结构来存储已经拷贝过的对象。你可以选择使用MapSet来实现这个数据结构,其中键是原始对象,值是拷贝后的对象。

我这边使用weakMap进行存储,因为weakMap的键是弱引用,这意味着当原始对象被垃圾回收时,与之相关联的缓存数据也会被自动释放。这可以避免内存泄漏问题,因为不再需要的缓存数据会自动清除,而不会导致缓存对象一直占用内存。

js 复制代码
// 为了共享map,将Map对象作为参数传递进来,
// 可以确保它在不同的递归调用之间是共享的。这意味着在整个深拷贝过程中,所有递归调用都使用相同的缓存对象

function deepClone(obj, map = new WeakMap()) {
  if (!isObject(obj)) {
    return obj;
  }

  // 如果存在循环引用,就返回之前所保存的引用
  if (map.has(obj)) {
    return map.get(obj);
  }

  let newObject = {};

  // 保存引用
  map.set(obj, newObject);
  
  if (obj instanceof Array) {
    newObject = [];
  }
  for (const key in obj) {
    newObject[key] = deepClone(obj[key], map);
  }
  return newObject;
}

4、完整版深拷贝

js 复制代码
// 判断是否是对象的工具函数
function isObject(value) {
  const valueType = typeof value;
  return (
    valueType !== null && (valueType === "object" || valueType === "function")
  );
}

function deepClone(obj, map = new WeakMap()) {
  // 0.[其他类型处理]如果是symbol,应该重新new一个
  if (typeof obj === "symbol") {
    const newSymbol = Symbol(obj.description);
    return newSymbol;
  }

  // 1.[其他类型处理]如果是原始数据类型就返回
  if (!isObject(obj)) {
    return obj;
  }

  // 2.[其他类型处理]如果是function,就直接返回(函数没必要深拷贝)
  if (typeof obj === "function") {
    return obj;
  }

  // 3.[其他类型处理]如果是set,因为set无法被for..in..遍历
  if (obj instanceof Set) {
    const newSet = new Set();
    for (const setItem of obj) {
      newSet.add(setItem);
    }
    return newSet;
  }
  // [循环引用处理]如果存在循环引用,就返回之前所保存的引用
  if (map.get(obj)) {
    return map.get(obj);
  }

  // 4.[其他类型处理]如果是数组初始数据就用[], 对象就用{}
  const newObject = obj instanceof Array ? [] : {};

  // [循环引用处理]保存引用(解决循环引用问题)
  map.set(obj, newObject);

  // 5.1.遍历普通的key
  for (const key in obj) {
    newObject[key] = deepClone(obj[key], map);
  }

> > > >   // 5.2.[其他类型处理]当symbol作为key时会被忽略,所以单独遍历symbol
  const symbolKeys = Object.getOwnPropertySymbols(obj);
  for (const symbolKey of symbolKeys) {
    newObject[Symbol(symbolKey.description)] = deepClone(obj[symbolKey], map);
  }
  return newObject;
}

5.总结

总之,通过实现深拷贝,我们能够深入研究数据类型和它们的特性,以及优化程序性能。尽管在实际开发中我们可能能够依赖现有的深拷贝工具,但通过亲自实现深拷贝,我们能够更全面地掌握相关概念和技术。

相关推荐
麦兜*2 分钟前
Spring Boot 集成Reactive Web 性能优化全栈技术方案,包含底层原理、压测方法论、参数调优
java·前端·spring boot·spring·spring cloud·性能优化·maven
知了一笑12 分钟前
独立开发第二周:构建、执行、规划
java·前端·后端
UI前端开发工作室1 小时前
数字孪生技术为UI前端提供新视角:产品性能的实时模拟与预测
大数据·前端
Sapphire~1 小时前
重学前端004 --- html 表单
前端·html
遇到困难睡大觉哈哈1 小时前
CSS中的Element语法
前端·css
Real_man1 小时前
新物种与新法则:AI重塑开发与产品未来
前端·后端·面试
小彭努力中1 小时前
147.在 Vue3 中使用 OpenLayers 地图上 ECharts 模拟飞机循环飞行
前端·javascript·vue.js·ecmascript·echarts
老马聊技术1 小时前
日历插件-FullCalendar的详细使用
前端·javascript
咔咔一顿操作2 小时前
Cesium实战:交互式多边形绘制与编辑功能完全指南(最终修复版)
前端·javascript·3d·vue
LuckyLay3 小时前
使用 Docker 搭建 Rust Web 应用开发环境——AI教你学Docker
前端·docker·rust