封装深拷贝方法

前言

在今年的四月份我写了一篇有关深拷贝的博客文章 我与深拷贝_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);// 拷贝Setif (tag == setTag) {value.forEach(subValue => {result.add(cloneDeep(subValue, value, stack));});}// 拷贝Mapelse 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相同大小的ArrayBufferconst 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相同大小的ArrayBufferconst 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.mzph.cn/bicheng/66449.shtml

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

相关文章

在不到 5 分钟的时间内将威胁情报 PDF 添加为 AI 助手的自定义知识

作者&#xff1a;来自 Elastic jamesspi 安全运营团队通常会维护威胁情报报告的存储库&#xff0c;这些报告包含由报告提供商生成的大量知识。然而&#xff0c;挑战在于&#xff0c;这些报告的内容通常以 PDF 格式存在&#xff0c;使得在处理安全事件或调查时难以检索和引用相关…

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

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

css出现边框

前言 正常情况下&#xff0c;开启 contenteditable 属性后会出现 “黑色边框”。 如下图所示&#xff0c;很影响美观&#xff1a; 您可能想去掉它&#xff0c;就像下面这样&#xff1a; 解决方案 通过选择器&#xff0c;将 focus 聚焦时移除 outline 属性即可。 如下代码所示&a…

恋爱脑学编程之C++模板编程大冒险

一、模板编程初相识&#xff1a;开启泛型编程魔法之旅 嘿&#xff0c;小伙伴们&#xff01;今天咱们要一起探索C中超级厉害的模板编程。它就像是一把万能钥匙&#xff0c;可以打开各种类型数据的大门&#xff0c;让咱们写出超酷的与类型无关的代码&#xff0c;大大提高代码的复…

enzymejest TDD与BDD开发实战

一、前端自动化测试需要测什么 1. 函数的执行逻辑&#xff0c;对于给定的输入&#xff0c;输出是否符合预期。 2. 用户行为的响应逻辑。 - 对于单元测试而言&#xff0c;测试粒度较细&#xff0c;需要测试内部状态的变更与相应函数是否成功被调用。 - 对于集成测试而言&a…

继承(5)

大家好&#xff0c;今天我们继续来学习继承的相关知识&#xff0c;来看看子类构造方法&#xff08;也叫做构造器&#xff09;是如何做的。 1.6 子类构造方法 父子父子,先有父再有子,即:子类对象构选时,需要先调用基类构造方法,然后执行子类的构造方法 ★此时虽然执行了父类的…

VulnHub-Acid(1/100)

参考链接&#xff1a; ​​​​​​​【VulnHub】Acid靶场复盘-CSDN博客 靶场渗透&#xff08;二&#xff09;——Acid渗透_ambassador 靶场渗透-CSDN博客 网络安全从0到0.5之Acid靶机实战渗透测试 | CN-SEC 中文网 Vulnhub靶场渗透练习(四) Acid - 紅人 - 博客园 红日团队…

51c自动驾驶~合集45

我自己的原文哦~ https://blog.51cto.com/whaosoft/13020031 #运动控制和规划控制需要掌握的技术栈~ 各大垃圾家电造车厂又要开始了~~~​ 1、ROS的通信方式 李是Lyapunov的李&#xff1a;谈谈ROS的通信机制 话题通信和服务通信&#xff0c;其中话题通信是通过发布和订阅…

【Unity3D】AB包加密(AssetBundle加密)

加密前&#xff1a; 加密后&#xff0c;直接无法加载ab&#xff0c;所以无法正常看到ab内容。 using UnityEngine; using UnityEditor; using System.IO; public static class AssetBundleDemoTest {[MenuItem("Tools/打包!")]public static void Build(){//注意:St…

平面坐标转大地坐标(arcgisPro中进行)

1、将需要转换的红线导入arcgisPro中&#xff0c;如下&#xff1a; 2、在地图菜单栏中&#xff0c;选择坐标转换工具&#xff0c;如下&#xff1a; 3、打开坐标转换工具 4、开启捕捉 5、 设置大地坐标显示格式 6、如下&#xff1a; 7、显示如图&#xff1a; 8、再依次添加几个待…

Python 在PDF中添加数字签名

在数字化时代&#xff0c;文件的安全性和可信度变得尤为重要。无论是商业文件、法律文件还是个人文件&#xff0c;都可能需要证明其来源的真实性和完整性。PDF数字签名就是解决这些问题的关键工具。PDF数字签名不仅能够确保文件的安全性&#xff0c;还能提高工作效率&#xff0…

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

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

STM32学习(十)

I2C模块内部结构 I2C&#xff08;Inter-Integrated Circuit&#xff09;模块是一种由Philips公司开发的二线式串行总线协议&#xff0c;用于短距离通信&#xff0c;允许多个设备共享相同的总线‌。 ‌硬件连接简单‌&#xff1a;I2C通信仅需要两条总线&#xff0c;即SCL&…

尚硅谷· vue3+ts 知识点学习整理 |14h的课程(持续更ing)

vue3 主要内容 核心&#xff1a;ref、reactive、computed、watch、生命周期 常用&#xff1a;hooks、自定义ref、路由、pinia、miit 面试&#xff1a;组件通信、响应式相关api ----> 笔记&#xff1a;ts快速梳理&#xff1b;vue3快速上手.pdf 笔记及大纲 如下&#xff…

【Ubuntu20.04】Apollo10.0 Docker容器部署+常见错误解决

官方参考文档【点击我】 Apollo 10.0 版本开始&#xff0c;支持本机和Docker容器两种部署方式。 如果您使用本机部署方式&#xff0c;建议使用x86_64架构的Ubuntu 22.04操作系统或者aarch64架构的Ubuntu 20.04操作系统。 如果您使用Docker容器部署方式&#xff0c;可以使用x…

安卓14无法安装应用解决历程

客户手机基本情况&#xff1a; 安卓14&#xff0c;对应的 targetSdkVersion 34 前天遇到了安卓14适配问题&#xff0c;客户发来的截图是这样的 描述&#xff1a;无法安装我们公司的B应用。 型号&#xff1a;三星google美版 解决步骤&#xff1a; 1、寻找其他安卓14手机测试…

利用 NineData 实现 PostgreSQL 到 Kafka 的高效数据同步

记录一次 PostgreSQL 到 Kafka 的数据迁移实践。前段时间&#xff0c;NineData 的某个客户在一个项目中需要将 PostgreSQL 的数据实时同步到 Kafka。需求明确且普遍&#xff1a; PostgreSQL 中的交易数据&#xff0c;需要实时推送到 Kafka&#xff0c;供下游多个系统消费&#…

Zookeeper是如何保证事务的顺序一致性的?

大家好&#xff0c;我是锋哥。今天分享关于【Zookeeper是如何保证事务的顺序一致性的?】面试题。希望对大家有帮助&#xff1b; Zookeeper是如何保证事务的顺序一致性的? 1000道 互联网大厂Java工程师 精选面试题-Java资源分享网 Zookeeper 通过多个机制来保证事务的顺序一…

电脑如何无线控制手机?

想在电脑上无线控制手机&#xff0c;需要用到Total Control控制软件&#xff0c;具体步骤如下&#xff1a; 1、首先我们在电脑上安装上控制软件Total Control并打开。 2、开启手机USB调试和ADB仅充电模式。 3、手机电脑均连接上相同局域网。 4、连接(首次使用需要用手机U…

内网穿透的应用-自托管文件分享系统PicoShare搭建流程与远程共享实战教程

文章目录 前言1. 本地安装Docker2. 本地部署PicoShare3. 如何使用PicoShare4. 公网远程访问本地 PicoShare4.1 内网穿透工具安装4.2 创建远程连接公网地址 5. 固定PicoShare公网地址 前言 大家好&#xff01;在数字化时代&#xff0c;文件共享变得越来越重要&#xff0c;尤其是…