封装深拷贝方法

news2025/1/8 16:16:46

前言

在今年的四月份我写了一篇有关深拷贝的博客文章 我与深拷贝_radash 深拷贝-CSDN博客。在该文章中有一个令我感到遗憾的点就是我没有实现一个自己手写的深拷贝。如今我想弥补当初的遗憾,在这篇文章中详细的讲述一下如何手写一个深拷贝方法。

lodash中是如何实现深拷贝的?

代码整理

我首先参考了lodash中的深拷贝方法,对它的代码进行了整理,整理后的代码如下所示。可以看到经过我整理后的代码依旧十分复杂,并且这个深拷贝方法中还调用了许多的其它方法,这些被调用的方法我还都没展示出来,在后续详细分析深拷贝的实现方式时,我再一 一介绍这些方法。

/** `Object#toString` result references. */
const argsTag = "[object Arguments]",
  arrayTag = "[object Array]",
  boolTag = "[object Boolean]",
  dateTag = "[object Date]",
  errorTag = "[object Error]",
  funcTag = "[object Function]",
  genTag = "[object GeneratorFunction]",
  mapTag = "[object Map]",
  numberTag = "[object Number]",
  objectTag = "[object Object]",
  regexpTag = "[object RegExp]",
  setTag = "[object Set]",
  stringTag = "[object String]",
  symbolTag = "[object Symbol]",
  weakMapTag = "[object WeakMap]";

const arrayBufferTag = "[object ArrayBuffer]",
  dataViewTag = "[object DataView]",
  float32Tag = "[object Float32Array]",
  float64Tag = "[object Float64Array]",
  int8Tag = "[object Int8Array]",
  int16Tag = "[object Int16Array]",
  int32Tag = "[object Int32Array]",
  uint8Tag = "[object Uint8Array]",
  uint8ClampedTag = "[object Uint8ClampedArray]",
  uint16Tag = "[object Uint16Array]",
  uint32Tag = "[object Uint32Array]";

/** Used to identify `toStringTag` values supported by `_.clone`. */
const cloneableTags = {};
cloneableTags[argsTag] =
  cloneableTags[arrayTag] =
  cloneableTags[arrayBufferTag] =
  cloneableTags[dataViewTag] =
  cloneableTags[boolTag] =
  cloneableTags[dateTag] =
  cloneableTags[float32Tag] =
  cloneableTags[float64Tag] =
  cloneableTags[int8Tag] =
  cloneableTags[int16Tag] =
  cloneableTags[int32Tag] =
  cloneableTags[mapTag] =
  cloneableTags[numberTag] =
  cloneableTags[objectTag] =
  cloneableTags[regexpTag] =
  cloneableTags[setTag] =
  cloneableTags[stringTag] =
  cloneableTags[symbolTag] =
  cloneableTags[uint8Tag] =
  cloneableTags[uint8ClampedTag] =
  cloneableTags[uint16Tag] =
  cloneableTags[uint32Tag] =
    true;
cloneableTags[errorTag] =
  cloneableTags[funcTag] =
  cloneableTags[weakMapTag] =
    false;

// T-深拷贝方法
function cloneDeep(value, object, stack) {
  let result;

  if (!isObject(value)) {
    return value;
  }

  // 获取数据类型标签
  const tag = getTag(value);

  // 做克隆的初始化准备工作
  if (Array.isArray(value)) {
    result = initCloneArray(value);
  } else {
    // value是否为函数
    const isFunc = tag == funcTag || tag == genTag;

    if (tag == objectTag || tag == argsTag || (isFunc && !object)) {
      result = isFunc ? {} : initCloneObject(value);
    } else {
      if (!cloneableTags[tag]) {
        return object ? value : {};
      }

      result = initCloneByTag(value, tag);
    }
  }

  // 检查循环引用并返回它相关的克隆副本
  stack = stack || new Map();
  const stacked = stack.get(value);
  if (stacked) {
    return stacked;
  }
  stack.set(value, result);

  // 拷贝Set
  if (tag == setTag) {
    value.forEach(subValue => {
      result.add(cloneDeep(subValue, value, stack));
    });
  }
  // 拷贝Map
  else if (tag == mapTag) {
    value.forEach((subValue, key) => {
      result.set(key, cloneDeep(subValue, value, stack));
    });
  }

  if (tag == objectTag || tag == arrayTag || tag == argsTag) {
    const props = getAllKeys(value);

    props.forEach(prop => {
      result[prop] = cloneDeep(value[prop], value, stack);
    });

  }

  return result;
}

代码结构分析

下面开始分析介绍cloneDeep方法的内容。首先看方法的参数,共有三个分别为valueobjectstack。其中value是需要进行深拷贝的值,object是需要拷贝的值的父级对象,stack是用于防止循环引用的缓存栈。当然我们只需要关注第一个参数value,后两个参数是在递归调用的时候才会起作用。

处理普通类型

接着开始处理value,首先第一步就可以根据value的数据类型大致分为两种情况:

要知道普通类型的数据不存在拷贝的问题,所以如果“value是普通类型数据”我们直接return value就可以了,对应到cloneDeep方法中 就是下面的这部分代码:

其中使用isObject方法识别数据是普通类型还是引用类型。

// 数据是否为类对象
function isObject(value) {
  return (
    (typeof value == "object" || typeof value == "function") && value !== null
  );
}

处理引用类型

下面第二步就要开始考虑“value是引用数据类型”的情况了。是不是直接就可以开始深拷贝啦?

当然不是,接下来cloneDeep中没有开始进行深拷贝,而是执行了一段复杂的代码,我将其称为“深拷贝前的过渡工作”。

为什么要先进行过渡工作呢?主要是因为其实不是所有的引用数据类型都需要深拷贝的。引用类型可以分为以下的几种情况:

  1. 需要进行深拷贝

在lodash的cloneDeep方法中就只会对ObjectArrayMapSetArguments五种类型进行深拷贝,因为这五种类型的数据中还有可能会存储其它的引用类型数据,这五种类型就是需要进行深拷贝的情况。

如果value是需要进行深拷贝的引用类型数据,那么在“过渡阶段”会进行一个克隆容器的准备工作,例如要克隆一个对象就要先准备一个空对象作为克隆容器,要克隆数组就要准备一个空数组作为克隆容器。

  • 如果valueObjectArguments类型,就会调用initCloneObject方法创建空对象。

initCloneObject方法中有一个需要注意的点,在创建克隆容器对象的时候,保证了它的原型与原对象是一致的。

// 初始化一个对象克隆
function initCloneObject(object) {
  return typeof object.constructor == "function"
    ? 
      Object.create(Object.getPrototypeOf(object)) //保证对象克隆的原型不变
    : {};
}
  • 如果valueArray类型则会调用initCloneArray方法创建克隆容器数组。

// 初始化一个数组克隆
function initCloneArray(array) {
  const length = array.length;
  const result = Array(length);

  //添加由正则表达式(RegExp)对象的 exec 方法所赋予的相关属性
  if (length && typeof array[0] === "string" && Reflect.has(array, "index")) {
    result.index = array.index;
    result.input = array.input;
  }

  return result;
}
  • 如果valueMapSet类型,就会在initCloneByTag方法中完成对克隆容器的准备。
  1. 只需要进行浅拷贝

有一些引用类型只会存储普通类型的数据或者就不会存储其它数据,对它们来说只需要进行浅拷贝就行了,例如TypedArray类型只会存储二进制数据所以根本就不需要深拷贝。在lodash的cloneDeep方法中DateRegExpArrayBufferDataViewTypedArray这几种类型就会被归在这类情况中。

怎么对这些类型进行浅拷贝呢?具体的实现方式各不相同,但大致的思路都是用构造函数重新创建一个与原数据一模一样的克隆数据,主要是在initCloneByTag方法中实现。

// 基于tag初始化一个对象克隆
function initCloneByTag(value, tag) {
  const Ctor = value.constructor; // 获取构造函数
  switch (tag) {
    case "[object ArrayBuffer]":
      return cloneArrayBuffer(value);
    case "[object Boolean]":
    case "[object Date]":
      return new Ctor(+value);

    case "[object DataView]":
      return cloneDataView(value);
    case "[object Float32Array]":
    case "[object Float64Array]":
    case "[object Int8Array]":
    case "[object Int16Array]":
    case "[object Int32Array]":
    case "[object Uint8Array]":
    case "[object Uint8ClampedArray]":
    case "[object Uint16Array]":
    case "[object Uint32Array]":
      return cloneTypedArray(value);

    case "[object Map]":
      return new Ctor();
    case "[object Number]":
    case "[object String]":
      return new Ctor(value);
    case "[object RegExp]":
      return cloneRegExp(value);
    case "[object Set]":
      return new Ctor();
    case "[object Symbol]":
      return cloneSymbol(value);
  }
}
// t-克隆方法
// 克隆ArrayBuffer
function cloneArrayBuffer(arrayBuffer) {
  // 创建一个与arrayBuffer相同大小的ArrayBuffer
  const duplicateArrayBuffer = new ArrayBuffer(arrayBuffer.byteLength);
  // 将arrayBuffer的数据复制到duplicateArrayBuffer中
  new Uint8Array(duplicateArrayBuffer).set(new Uint8Array(arrayBuffer));
  return duplicateArrayBuffer;
}

//克隆DataView
function cloneDataView(dataView) {
  const buffer = cloneArrayBuffer(dataView.buffer);
  return new DataView(buffer, dataView.byteOffset, dataView.byteLength);
}

// 克隆TypedArray
function cloneTypedArray(typedArray) {
  const buffer = cloneArrayBuffer(typedArray.buffer);
  return new typedArray.constructor(
    buffer,
    typedArray.byteOffset,
    typedArray.length
  );
}

// 克隆RegExp
function cloneRegExp(regExp) {
  const result = new RegExp(regExp.source, regExp.flags);
  result.lastIndex = regExp.lastIndex;
  return result;
}

// 克隆Symbol
function cloneSymbol(symbol) {
  return Symbol(symbol.description);
}
  1. 无法进行拷贝

在lodash的实现中标定了FunctionGeneratorFunctionErrorWeakMap这几种类型是无法进行拷贝的。

对于这类数据如果它没有父级对象会返回一个空对象,如果它有父级对象则会返回它本身。我也不清楚为什么lodash中要这样设计为两种情况返回不同的值。

我猜测可能是这样,比如说直接将一个函数传入cloneDeep方法中会返回{},目的可能是为了提醒使用者该数据是无法深拷贝的。而当我把函数放到一个对象里,再把对象传入cloneDeep中,此时其实是在克隆对象,为了保证克隆后对象内的成员完整,所以要返回函数本身。

const func = ()=>{}
cloneDeep(func) // => {}
cloneDeep({a:func}) // => {a:()=>{}}
  1. 没有考虑

还有一些类型可能在封装深拷贝方法的时候都没有考虑到,例如克隆DOM元素等。这种情况与“无法进行拷贝”走同样的处理逻辑。

通过递归实现深拷贝

cloneDeep中剩余的代码就是深拷贝的部分了,它们是整个深拷贝函数中最核心的代码,这部分代码可以划分为下图中的四个部分:

这部分代码中的一些基础的部分(深拷贝的原理、循环引用等)我也就不详细讲了,不懂的可以去查阅其它的资料。这部分代码有两个值得提一下的点:

  1. 对象数组共用同一套代码进行深拷贝

在lodash的深拷贝实现中,对象和数组使用同一套代码进行深拷贝。基本的思路是先通过一个getAllKeys方法获取对象/数组的键数组,然后遍历键数组实现深拷贝。

// 获取对象的所有属性名
function getAllKeys(obj) {
  return getTag(obj) === "[object Object]"
    ? Object.getOwnPropertyNames(obj).concat(Object.getOwnPropertySymbols(obj))
    : Object.keys(obj);
}
  1. 如何拷贝Arguments

在lodash的深拷贝实现中支持对Arguments的拷贝,在具体实现的时候是将Arguments类型的数据看做一个对象进行拷贝的。

const testArgsClone = function (a, b) {
  const argsClone = cloneDeep(arguments);
  console.log(arguments); // [Arguments] { '0': 1, '1': 2 }
  console.log(getTag(arguments)); //[object Arguments]
  console.log(argsClone); //{ '0': 1, '1': 2 }
  console.log(getTag(argsClone)); //[object Object]
};
testArgsClone(1, 2);

对各种类型数据的处理方式总结

类型

处理方式

数字、字符串、布尔值

不克隆:返回自身

Symbol

不克隆:返回自身

null、undefined

不克隆:返回自身

日期

专门方法克隆

正则

专门方法克隆

ArrayBuffer

专门方法克隆

DataView

专门方法克隆

TypeArray

专门方法克隆

数组

递归克隆

对象

递归克隆

Map

递归克隆

Set

递归克隆

Arguments

递归克隆,但是注意,得到的克隆副本是一个存储着参数的普通对象

函数、生成器函数

无法克隆:

  • 如果是一个单独的数据,返回{}
  • 如果是对象数组等类型中嵌套的数据,则返回自身

错误

无法克隆:

  • 如果是一个单独的数据,返回{}
  • 如果是对象数组等类型中嵌套的数据,则返回自身

WeakMap

无法克隆:

  • 如果是一个单独的数据,返回{}
  • 如果是对象数组等类型中嵌套的数据,则返回自身

其他相关问题

1.对象属性的特性是否会被克隆

经过测试后发现并不会克隆属性的描述符(属性特性)

const obj = {};
Object.defineProperty(obj, "prop", {
  value: 1,
  writable: false,
  enumerable: true,
  configurable: true,
});

const objClone = cloneDeep(obj,);
console.log(Object.getOwnPropertyDescriptor(obj, "prop")); // { value: 1, writable: false, enumerable: true, configurable: true } 
console.log(Object.getOwnPropertyDescriptor(objClone, "prop")); // { value: 1, writable: true, enumerable: true, configurable: true }

我的深拷贝

我的实现方式

根据上面总结的经验,我封装了自己的深拷贝方法,与lodash的实现方式不同的地方主要有几点:

  1. 对所有不需要克隆、无法克隆和没有考虑到的值都直接返回;
  2. 对象和数组的克隆不在放在一起进行了;
  3. Arguments视为数组进行处理。

const getType = value => {
    return Object.prototype.toString
    .call(value)
    .match(/(?<=\[object )(\w+)(?=\])/)[0];
};

function cloneDeep(value, stack = new Map()) {
  const type = getType(value);

  // 需要特殊处理的类型
  if (type === "Date") {
    return new Date(value.getTime());
  } else if (type === "RegExp") {
    return new RegExp(value.source, value.flags);
  } else if (type === "ArrayBuffer") {
    return value.slice();
  } else if (type === "DataView") {
    const buffer = value.buffer.slice();
    return new DataView(buffer, value.byteOffset, value.byteLength);
  } else if (
    [
      "Int8Array",
      "Uint8Array",
      "Uint8ClampedArray",
      "Int16Array",
      "Uint16Array",
      "Int32Array",
      "Uint32Array",
      "Float32Array",
      "Float64Array",
    ].includes(type)
  ) {
    const buffer = value.buffer.slice();
    return new value.constructor(buffer, value.byteOffset, value.length);
  }

  // 递归进行深拷贝
  let result = stack.get(value);
  if (result) return result;

  if (type === "Set") {
    result = new Set();
    stack.set(value, result);
    value.forEach(item => {
      result.add(cloneDeep(item, stack));
    });
    return result;
  } else if (type === "Map") {
    result = new Map();
    stack.set(value, result);
    value.forEach((item, key) => {
      result.set(key, cloneDeep(item, stack));
    });
    return result;
  } else if (type === "Object") {
    result = Object.create(Object.getPrototypeOf(value));
    stack.set(value, result);

    const keys = Reflect.ownKeys(value);

    keys.forEach(key => {
      result[key] = cloneDeep(value[key], stack);
    });

    return result;
  } else if (type === "Array" || type === "Arguments") {
    result = [];
    stack.set(value, result);
    for (let i = 0; i < value.length; i++) {
      result[i] = cloneDeep(value[i], stack);
    }
    return result;
  }
  return value;
}

对于一些问题的探讨

1.ArrayBuffer、DataView、TypedArray的克隆方式

lodash中克隆这三种类型的数据的方法如下:

// 克隆ArrayBuffer
function cloneArrayBuffer(arrayBuffer) {
  // 创建一个与arrayBuffer相同大小的ArrayBuffer
  const duplicateArrayBuffer = new ArrayBuffer(arrayBuffer.byteLength);
  // 将arrayBuffer的数据复制到duplicateArrayBuffer中
  new Uint8Array(duplicateArrayBuffer).set(new Uint8Array(arrayBuffer));
  return duplicateArrayBuffer;
}

//克隆DataView
function cloneDataView(dataView) {
  const buffer = cloneArrayBuffer(dataView.buffer);
  return new DataView(buffer, dataView.byteOffset, dataView.byteLength);
}

// 克隆TypedArray
function cloneTypedArray(typedArray) {
  const buffer = cloneArrayBuffer(typedArray.buffer);
  return new typedArray.constructor(
    buffer,
    typedArray.byteOffset,
    typedArray.length
  );
}

我在实现的时候有一些不同,简单来说就是使用ArrayBufferslice方法来对其进行克隆。这样就不用手动去拷贝字节了,简化了代码。

新的实现:

// 深拷贝ArrayBuffer
function cloneDeepArrayBuffer(arrayBuffer) {
  return arrayBuffer.slice();
}
// 深拷贝DataView
function cloneDeepDataView(dataView) {
  const buffer = dataView.buffer.slice();
  return new DataView(buffer, dataView.byteOffset, dataView.byteLength);
}

// 深拷贝TypedArray
function cloneDeepTypedArray(typedArray) {
  const buffer = typedArray.buffer.slice();
  return new typedArray.constructor(
    buffer,
    typedArray.byteOffset,
    typedArray.length
  );
}

2.Symbol类型是否需要深拷贝?

这是一个让我感到十分疑惑的问题,我在阅读《如何写出一个惊艳面试官的深拷贝?》这篇文章时就发现,作者在实现深拷贝的过程中还写了对SymbolStringBoolean等普通类型拷贝的逻辑。

并且lodash中的initCloneByTag方法中也是有类似的逻辑的:

只是有一个小问题,像Symbol等普通类型的数据,应该在下面的这一步就被处理了,根本到不了initCloneByTag

这个就很奇怪了,后来我看到了那篇文章的评论区中有如下的解释:

我进行了测试,发现好像并不是想他说的那样有所谓的基本类型和包装类型的区别。

const n1 = 10;
const n2 = Number(10);

console.log(typeof n1, Object.prototype.toString.call(n1));//number [object Number]
console.log(typeof n2, Object.prototype.toString.call(n2));//number [object Number]

但是不对,我好像写错了应该是new Number(10)而不是Number(10):

const n1 = 10;
const n2 = new Number(10);

console.log(typeof n1, Object.prototype.toString.call(n1)); //number [object Number]
console.log(typeof n2, Object.prototype.toString.call(n2)); //object [object Number]

这样就对了,所以用构造函数创建出来的普通类型数据typeof是无法识别它们真正的类型的,并且由于它们也是object,所以也要对它们进行克隆以保证引用不同。

当然我并不打算修改我的深拷贝方法,因为首先我觉得使用字面量的方式创建还是用构造函数创建其实并没有区别,并不会存在深拷贝的问题,其次想要对这些包装类型进行处理得大规模的修改我的代码结构,我并不想这样做,并且由于在我的代码中并没有使用typeof去判断数据类型,所以也不存在误判的情况。

3.使用weakMap替代Map进行缓存

在我们的实现当中是使用一个Map作为缓存栈来解决循环引用的问题的,而在《如何写出一个惊艳面试官的深拷贝?》中作者提到可以用WeakMap替代Map,按照作者的意思使用Map会对内存造成非常大的额外消耗,而且我们需要手动清除Map的属性才能释放这块内存,而WeakMap会帮我们巧妙化解这个问题,因为WeakMap是弱引用,当没用其它地方引用WeakMap中的对象时,这些对象就会被垃圾回收机制所回收。

然而实际上这种观点在评论区中被很多读者所反对,认为这么做就是画蛇添足。因为在作者(包括我)的实现中Map都是作为函数内的一个参数的,参数是函数内的局部变量,在函数执行完毕后就会被回收。所以根本不需要使用WeakMap

不过我发现这个东西可能对我来说还真有用处,因为我现在不想将stack作为cloneDeep方法的参数,因为cloneDeep方法我将来是要导出去使用的,万一在使用的时候不小心传了第二个参数,那么cloneDeep方法岂不就要出问题,所以我想将stack提出来使用:

const stack = new Map()

function cloneDeep(value){
  ......
}

如果像上面这样写,那么stack里面的值就真的回收不了了,这个时候就应该使用WeakMap来实现这样一个缓存栈的功能:

const cacheStack = new WeakMap(); //缓存栈

function cloneDeep(value) {
  const type = getType(value);

  // 需要特殊处理的类型
  if (type === "Date") {
    return new Date(value.getTime());
  } else if (type === "RegExp") {
    return new RegExp(value.source, value.flags);
  } else if (type === "ArrayBuffer") {
    return value.slice();
  } else if (type === "DataView") {
    const buffer = value.buffer.slice();
    return new DataView(buffer, value.byteOffset, value.byteLength);
  } else if (
    [
      "Int8Array",
      "Uint8Array",
      "Uint8ClampedArray",
      "Int16Array",
      "Uint16Array",
      "Int32Array",
      "Uint32Array",
      "Float32Array",
      "Float64Array",
    ].includes(type)
  ) {
    const buffer = value.buffer.slice();
    return new value.constructor(buffer, value.byteOffset, value.length);
  }

  // 递归进行深拷贝
  let result = cacheStack.get(value);
  if (result) return result;

  if (type === "Set") {
    result = new Set();
    cacheStack.set(value, result);
    value.forEach(item => {
      result.add(cloneDeep(item));
    });
    return result;
  } else if (type === "Map") {
    result = new Map();
    cacheStack.set(value, result);
    value.forEach((item, key) => {
      result.set(key, cloneDeep(item));
    });
    return result;
  } else if (type === "Object") {
    result = Object.create(Object.getPrototypeOf(value));
    cacheStack.set(value, result);

    const keys = Reflect.ownKeys(value);

    keys.forEach(key => {
      result[key] = cloneDeep(value[key]);
    });

    return result;
  } else if (type === "Array" || type === "Arguments") {
    result = [];
    cacheStack.set(value, result);
    for (let i = 0; i < value.length; i++) {
      result[i] = cloneDeep(value[i]);
    }
    return result;
  }
  return value;
}

4.递归转循环

我在文章在《如何写出一个惊艳面试官的深拷贝?》的评论区还看到很多读者提到可以用循环代替递归,我对这个很感兴趣,于是也自己尝试实现了一下。

"递归转循环"的原理其实是就是模拟函数的执行栈,例如有如下的一个递归函数:

function factorial(num){
  return  num > 1 ? num * factorial(--num) : 1
}

我们当然可以像下面这样写,从而将递归转为循环:

function factorial(num) {
  let result = 1;

  for (let i = num; i > 1; i--) {
    result *= i;
  }
  return result;
}

不过上面的这种方式并没有很好的实现“模拟函数执行栈”,我们可以改用下面这种写法。其中stack数组就相当于是函数的执行栈,stack中的数字就是需要执行的函数,只要执行栈stack中还有元素,那while循环就不会终止,只有当达到终止条件(n小于等于1)时循环才会终止,这里的循环终止条件其实就相当于是递归的终止条件,循环体中的内容就相当于是递归函数中的内容。

function factorial(num) {
  const stack = [num];
  let result = 1;

  while (stack.length) {
    let n = stack.pop();
    result *= n;
    if (n > 1) stack.push(--n);
  }

  return result;
}

基于上面的思路我将我之前写的深拷贝方法进行了“递归转循环”:

function getType(value) {
  return Object.prototype.toString
    .call(value)
    .match(/(?<=\[object )(\w+)(?=\])/)[0];
}

// 准备容器
function initContainer(value, type) {
  type = type || getType(value);
  switch (type) {
    case "Object":
      return Object.create(Object.getPrototypeOf(value));
    case "Array":
    case "Arguments":
      return [];
    case "Set":
      return new Set();
    case "Map":
      return new Map();
  }
}

// 根据类型克隆数据
function cloneByType(value, type) {
  type = type || getType(value);
  if (type === "Date") {
    return new Date(value.getTime());
  } else if (type === "RegExp") {
    return new RegExp(value.source, value.flags);
  } else if (type === "ArrayBuffer") {
    return value.slice();
  } else if (type === "DataView") {
    const buffer = value.buffer.slice();
    return new DataView(buffer, value.byteOffset, value.byteLength);
  } else if (
    [
      "Int8Array",
      "Uint8Array",
      "Uint8ClampedArray",
      "Int16Array",
      "Uint16Array",
      "Int32Array",
      "Uint32Array",
      "Float32Array",
      "Float64Array",
    ].includes(type)
  ) {
    const buffer = value.buffer.slice();
    return new value.constructor(buffer, value.byteOffset, value.length);
  }

  return value;
}

// 使用遍历方式进行深拷贝
function cloneDeep(value) {
  let result = initContainer(value);
  if (!result) return cloneByType(value);
  const cacheStack = new WeakMap(); //缓存栈
  const executionStack = [{ source: value, target: result }]; //执行栈

  // 用循环模拟递归的过程
  while (executionStack.length) {
    let { source, target } = executionStack.pop();
    // 缓存
    cacheStack.set(source, target);

    // 获取数据类型
    const type = getType(source);

    if (type === "Set") {
      source.forEach(item => {
        const subTarget = initContainer(item);
        if (subTarget) {
          if (cacheStack.has(item)) {
            target.add(cacheStack.get(item));
          } else {
            target.add(subTarget);
            executionStack.push({ source: item, target: subTarget });
          }
        } else {
          target.add(cloneByType(item));
        }
      });
    } else if (type === "Map") {
      source.forEach((item, key) => {
        const subTarget = initContainer(item);
        if (subTarget) {
          if (cacheStack.has(item)) {
            target.set(key, cacheStack.get(item));
          } else {
            target.set(key, subTarget);
            executionStack.push({ source: item, target: subTarget });
          }
        } else {
          target.set(key, cloneByType(item));
        }
      });
    } else if (type === "Object") {
      const keys = Reflect.ownKeys(source);
      keys.forEach(key => {
        const subTarget = initContainer(source[key]);
        if (subTarget) {
          if (cacheStack.has(source[key])) {
            target[key] = cacheStack.get(source[key]);
          } else {
            target[key] = subTarget;
            executionStack.push({ source: source[key], target: subTarget });
          }
        } else {
          target[key] = cloneByType(source[key]);
        }
      });
    } else if (type === "Array" || type === "Arguments") {
      for (let i = 0; i < source.length; i++) {
        const subTarget = initContainer(source[i]);
        if (subTarget) {
          if (cacheStack.has(source[i])) {
            target[i] = cacheStack.get(source[i]);
          } else {
            target[i] = subTarget;
            executionStack.push({ source: source[i], target: subTarget });
          }
        } else {
          target[i] = cloneByType(source[i]);
        }
      }
    }
  }

  return result;
}

参考资料

  1. https://github.com/lodash/lodash
  2. lodash.cloneDeep | Lodash中文文档 | Lodash中文网
  3. 一篇彻底搞定对象的深度克隆 | 包括function和symbol类型_克隆symbol-CSDN博客
  4. 如何写出一个惊艳面试官的深拷贝?
  5. ArrayBuffer.prototype.slice() - JavaScript | MDN
  6. 对象深拷贝—解决循环引用以及递归爆栈问题_递归遇到循环引用怎么办-CSDN博客

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2273313.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

maven多模块项目编译一直报Failure to find com.xxx.xxx:xxx-xxx-xxx:pom:1.0-SNAPSHOT in问题

工作中项目上因为多版本迭代&#xff0c;需要对不同迭代版本升级版本号&#xff0c;且因为项目工程本身是多模块结构&#xff0c;且依然多个其他模块工程。 在将工程中子模块的pom.xml中版本号使用变量引用父模块中定义的版本号时&#xff0c;一直报Failure to find com.xxx.x…

uniapp 微信小程序 自定义日历组件

效果图 功能&#xff1a;可以记录当天是否有某些任务或者某些记录 具体使用&#xff1a; 子组件代码 <template><view class"Accumulate"><view class"bx"><view class"bxx"><view class"plank"><…

Mysql--基础篇--函数(字符串函数,日期函数,数值函数,聚合函数,自定义函数及与存储过程的区别等)

MySQL提供了丰富的内置函数&#xff0c;涵盖了字符串处理、数值计算、日期和时间操作、聚合统计、控制流等多种功能。这些函数可以帮助你简化SQL查询&#xff0c;提升开发效率。 除了内置函数&#xff0c;MySQL还支持自定义函数&#xff08;User-Defined Functions&#xff09;…

关于Mac中的shell

1 MacOS中的shell 介绍&#xff1a; 在 macOS 系统中&#xff0c;Shell 是命令行与系统交互的工具&#xff0c;用于执行命令、运行脚本和管理系统。macOS 提供了多种 Shell&#xff0c;主要包括 bash 和 zsh。在 macOS Catalina&#xff08;10.15&#xff09;之前&#xff0c…

外卖院长帝恩以专业打法,开启外卖运营新格局

据中国饭店协会数据显示&#xff0c;2023年我国在线餐饮外卖市场规模已达到15254亿元&#xff0c;同比增长36.7%。预计到2027年&#xff0c;我国在线餐饮外卖市场规模有望达到19567亿元&#xff0c;行业渗透率有望达到30.4%。在这一蓬勃发展的行业背后&#xff0c;离不开无数从…

高清绘画素材3600多张动漫线稿线描上色练习参考插画原画

工作之余来欣赏一波线稿&#xff0c;不务正业版... 很多很多的线稿... 百度网盘 请输入提取码

Power BI如何连接Azure Databricks数据源?

故事背景: 近期有朋友询问&#xff0c;自己公司有一些项目使用了Azure Databricks用于数据存储。如何使用Power BI Desktop桌面开发软件连接Azure Databricks的数据源呢&#xff1f; 解决方案: 其实Power BI是提供了连接Azure Databricks数据源的选项的&#xff0c;只是配置…

了解RabbitMQ中的Exchange:深入解析与实践应用

在分布式系统设计中&#xff0c;消息队列&#xff08;Message Queue&#xff09;扮演着至关重要的角色&#xff0c;而RabbitMQ作为开源消息代理软件的佼佼者&#xff0c;以其高性能、高可用性和丰富的功能特性&#xff0c;成为了众多开发者的首选。在RabbitMQ的核心组件中&…

前端通过后端返回的数据流下载文件

后端返回文件流,下载的文件无法读取,损坏,原因是因为接口处理没有加 blob类型 downloadFile(row.fileId).then(res > { // res 即后端返回的数据流 const blob new Blob([res.data]) if (blob && blob.size 0) { this.$notify.error(内容为空&#xff0c;无法下载…

基于 GEE Sentinel-1 数据集提取水体

目录 1 水体提取原理 2 完整代码 3 运行结果 1 水体提取原理 水体提取是地理信息和遥感技术的关键应用之一&#xff0c;对于多个领域都具有重要的应用价值。它有助于更好地管理水资源&#xff0c;保护环境&#xff0c;减少灾害风险&#xff0c;促进可持续发展&#xff0c;以…

BloombergGPT: A Large Language Model for Finance——面向金融领域的大语言模型

这篇文章介绍了BloombergGPT&#xff0c;一个专门为金融领域设计的大语言模型&#xff08;LLM&#xff09;。以下是文章的主要内容总结&#xff1a; 背景与动机&#xff1a; 大语言模型&#xff08;如GPT-3&#xff09;在多个任务上表现出色&#xff0c;但尚未有针对金融领域的…

ansible-api分析(VariableManager变量)

一. 简述&#xff1a; ansible是一个非常强大的工具&#xff0c;可以支持多种类型(字符,数字,列表&#xff0c;字典等)的变量。除了有大量的内置变量及fact变量&#xff0c;也可以通过多种方式进行变量自定义 。不同方式定义的变量&#xff0c;优先级也不太一样&#xff0c;之…

2025年PMP考试最新报名通知

经PMI和中国国际人才交流基金会研究决定&#xff0c;中国大陆地区2025年第一期PMI认证考试定于3月15日举办。在基金会网站报名参加本次PMI认证考试的考生须认真阅读下文&#xff0c;知悉考试安排及注意事项&#xff0c;并遵守考试有关规定。 一、时间安排 &#xff08;一&#…

Mysql--基础篇--数据类型(整数,浮点数,日期,枚举,二进制,空间类型等)

MySQL提供了多种数据类型&#xff0c;用于定义表中列的数据格式。选择合适的数据类型不仅可以提高查询性能&#xff0c;还能确保数据的完整性和准确性。 一、数值类型 数值类型用于存储整数、浮点数和定点数。根据精度和范围的不同&#xff0c;数值类型可以分为以下几类&…

1-Transformer算法解读

目录 一.RNN与Transfrmer 二.word2vec 三.自注意力机制 四.辅助向量Q/K/V 五.计算过程 六.整体架构​编辑 七.Bert 一.RNN与Transfrmer RNN(循环神经网络)和Transformer都是深度学习中用于处理序列数据的模型,但它们在结构和性能上有显著的区别。以下是它们的一些…

Java Web开发进阶——Spring Boot与Spring Data JPA

Spring Data JPA 是 Spring 提供的一种面向数据访问的持久化框架&#xff0c;它简化了 JPA 的实现&#xff0c;为开发者提供了一种快速操作数据库的方式。在结合 Spring Boot 使用时&#xff0c;开发者能够快速完成数据库访问层的开发。 1. 介绍Spring Data JPA 1.1 什么是Spr…

【计算机操作系统:一、绪论】

第1章 绪论 1.1 操作系统在计算机系统中的地位 1.1.1 存储程序式计算机的结构和特点 存储程序式计算机&#xff08;Stored Program Computer&#xff09;是现代计算机的基础&#xff0c;其概念源于冯诺依曼&#xff08;John von Neumann&#xff09;提出的模型。这种计算机架…

如何查看服务器上的MySQL/Redis等系统服务状态和列表

如果呢你知道系统服务名称&#xff0c;要看状态很简单&#xff1a; systemctl status server-name 比如 systemctl status nginxsystemctl status redis # 等 这是一个nginx的示例&#xff1a; 那问题是 当你不知道服务名称时该怎么办。举个例子&#xff0c;比如mysql在启动…

安科瑞Acrel-1000DP分布式光伏监控系统在浙江安吉成3234.465kWp分布式光伏发电项目中的应用

摘 要&#xff1a;分布式光伏发电站是指将光伏发电组件安装在用户的建筑物屋顶、空地或其他适合的场地上&#xff0c;利用太阳能进行发电的一种可再生能源利用方式&#xff0c;与传统的大型集中式光伏电站相比&#xff0c;分布式光伏发电具有更灵活的布局、更低的建设成本和更高…

探索 Vue.js 的动态样式与交互:一个有趣的样式调整应用

修改日期备注2025.1.3初版 一、前言 今天和大家分享在 Vue.js 学习过程中开发的超酷的小应用。这个应用可以让我们通过一些简单的交互元素&#xff0c;如复选框、下拉菜单和输入框&#xff0c;来动态地改变页面上元素的样式哦 让我们一起深入了解一下这个项目的实现过程&…