重新思考deepClone

如您所知,JavaScript是通过引用复制对象的。 但是有时您需要对对象进行深克隆。 对于这种情况,许多js库提供了deepClone函数的实现。 但是,不幸的是,大多数库都没有考虑以下重要事项:

  • 数组可能位于对象中,最好将它们复制为数组
  • 该对象可能具有以符号为键的字段
  • 对象字段具有非默认描述符
  • 函数可以位于对象的字段中,也需要对其进行克隆。
  • 对象最终具有不同于Object.prototype的原型

谁破解了,我把完整的代码放在了扰流板下面
function deepClone(source) { return ({ 'object': cloneObject, 'function': cloneFunction }[typeof source] || clonePrimitive)(source)(); } function cloneObject(source) { return (Array.isArray(source) ? () => source.map(deepClone) : clonePrototype(source, cloneFields(source, simpleFunctor({}))) ); } function cloneFunction(source) { return cloneFields(source, simpleFunctor(function() { return source.apply(this, arguments); })); } function clonePrimitive(source) { return () => source; } function simpleFunctor(value) { return mapper => mapper ? simpleFunctor(mapper(value)) : value; } function makeCloneFieldReducer(source) { return (destinationFunctor, field) => { const descriptor = Object.getOwnPropertyDescriptor(source, field); return destinationFunctor(destination => Object.defineProperty(destination, field, 'value' in descriptor ? { ...descriptor, value: deepClone(descriptor.value) } : descriptor)); }; } function cloneFields(source, destinationFunctor) { return (Object.getOwnPropertyNames(source) .concat(Object.getOwnPropertySymbols(source)) .reduce(makeCloneFieldReducer(source), destinationFunctor) ); } function clonePrototype(source, destinationFunctor) { return destinationFunctor(destination => Object.setPrototypeOf(destination, Object.getPrototypeOf(source))); } 

我的实现以一种功能风格编写,为我提供了可靠性,稳定性和简单性。 但是,由于不幸的是,由于许多人仍然无法通过过程主义和伪OOP重建他们的思想,因此,我将解释我的实现过程中的每一个基本步骤:

deepClone函数本身将带有1个参数源-我们将从其进行克隆的源,并将返回具有上述所有功能的深层克隆:

 function deepClone(source) { return ({ 'object': cloneObject, 'function': cloneFunction }[typeof source] || clonePrimitive)(source)(); } 

这里的一切都很简单,取决于源中数据的类型,选择了一个可以克隆它的函数,然后将源本身转移给它。

您还可以注意到,返回的结果在被返回给用户之前被称为没有参数的函数。 这是必要的,因为我将克隆的值包装在最简单的函子中,以便能够对其进行突变而不会违反辅助功能的纯度。 这是此函子的实现:

 function simpleFunctor(value) { return mapper => mapper ? simpleFunctor(mapper(value)) : value; } 

他可以做两件事-映射(如果将mapper函数传递给他)和提取(如果不传递任何东西)。

现在,我们将分析辅助功能cloneObject,cloneFunction和clonePrimitive。 它们每个都接受特定类型source的1个参数并返回其克隆。

cloneObject的实现应考虑到数组也是object类型的,在其他情况下,它们必须克隆字段和原型。 这是它的实现:

 function cloneObject(source) { return (Array.isArray(source) ? () => source.map(deepClone) : clonePrototype(source, cloneFields(source, simpleFunctor({}))) ); } 

可以使用slice方法复制该数组,但是由于我们具有深度克隆功能,并且该数组不仅可以包含原始值,因此map方法与上述deepClone一起用作参数。

对于其他对象,我们创建一个新对象并将其包装在上述函子中,使用cloneFields帮助器函数克隆字段(以及描述符),然后使用clonePrototype克隆原型。

我将在下面描述的辅助函数。 同时,请考虑cloneFunction的实现:

 function cloneFunction(source) { return cloneFields(source, simpleFunctor(function() { return source.apply(this, arguments); })); } 

您根本无法使用所有逻辑克隆函数。 但是您可以将其包装在另一个函数中,该函数使用所有参数和上下文调用原始函数,并返回其结果。 这样的“克隆”当然可以将原始功能保留在内存中,但它会“重一些”并完全重现原始逻辑。 我们将克隆的函数包装在函子中,并使用cloneFields将所有函数从原始函数复制到函数中,因为JS中的函数也是一个对象,只是被调用,因此可以在其中存储字段。

一个函数可能具有与Function.prototype不同的原型,但是我没有考虑这种极端情况。 FP的魅力之一是,我们可以轻松地在现有功能上添加新包装,以实现必要的功能。

最后一个clonePrimitive建筑图块用于克隆基本值。 但是由于原始值是按值复制的(或按引用复制的,但是在JS引擎的某些实现中是不变的),所以我们可以简单地复制它们。 但是,由于我们不希望得到纯值,而是包装在函子中的值,该函子可以在提取时不带参数地进行调用,因此我们将值包装在函数中:

 function clonePrimitive(source) { return () => source; } 

现在,我们实现上面使用的辅助功能-clonePrototype和cloneFields

要克隆原型, clonePrototype将简单地从源对象中提取原型,并通过对生成的函子执行映射操作,将其设置为目标对象:

 function clonePrototype(source, destinationFunctor) { return destinationFunctor(destination => Object.setPrototypeOf(destination, Object.getPrototypeOf(source))); } 

克隆字段要复杂一些,因此我将cloneFields函数拆分为两个。 外部函数将所有命名字段和所有符号字段的连接,绝对接收所有字段,并通过辅助函数创建的reducer运行它们:

 function cloneFields(source, destinationFunctor) { return (Object.getOwnPropertyNames(source) .concat(Object.getOwnPropertySymbols(source)) .reduce(makeCloneFieldReducer(source), destinationFunctor) ); } 

makeCloneFieldReducer应该为我们创建一个reducer函数,该函数可以传递给源对象所有字段的数组上的reduce方法。 作为电池,将使用存储目标的函子。 减速器必须从源对象的字段中提取句柄,并将其分配给目标对象的字段。 但是在这里重要的是要考虑到有两种类型的描述符-带有值和带有get / set。 显然,需要克隆value,但是使用get / set不需要这样,可以按原样返回这样的描述符:

 function makeCloneFieldReducer(source) { return (destinationFunctor, field) => { const descriptor = Object.getOwnPropertyDescriptor(source, field); return destinationFunctor(destination => Object.defineProperty(destination, field, 'value' in descriptor ? { ...descriptor, value: deepClone(descriptor.value) } : descriptor)); }; } 

仅此而已。 deepClone的这种实现解决了本文开头提出的所有问题。 此外,它基于纯函数和一个函子构建,这为lambda微积分提供了所有固有的保证。

我还注意到,除了那些值得单独克隆的数组(例如Map或Set)之外,我没有为集合实现出色的行为。 尽管在某些情况下这可能是必需的。

Source: https://habr.com/ru/post/zh-CN466535/


All Articles