Repensando deepClone

Como você sabe em JavaScript, os objetos são copiados por referência. Mas às vezes você precisa fazer uma clonagem profunda de um objeto. Muitas bibliotecas js oferecem sua implementação da função deepClone para este caso. Infelizmente, porém, a maioria das bibliotecas não leva em consideração várias coisas importantes:

  • Matrizes podem estar no objeto e é melhor copiá-las como matrizes
  • O objeto pode ter campos com um símbolo como chave
  • Os campos de objeto têm descritores diferentes do padrão
  • As funções podem estar nos campos do objeto e também devem ser clonadas.
  • Um objeto finalmente possui um protótipo diferente de Object.prototype

Quem o quebrou, coloquei o código completo embaixo do spoiler
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))); } 

Minha implementação é escrita em um estilo funcional que me proporciona confiabilidade, estabilidade e simplicidade. Mas como, infelizmente, muitos ainda não conseguem reconstruir seu pensamento com procedimentalismo e pseudo-POO, explicarei todos os tijolos da minha implementação:

A função deepClone em si terá 1 fonte de argumento - a fonte da qual clonaremos e seu clone profundo com todos os recursos acima será retornado:

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

Tudo é simples aqui, dependendo do tipo de dados na fonte, é selecionada uma função que pode cloná-la e a própria fonte é transferida para ela.

Você também pode observar que o resultado retornado é chamado como uma função sem parâmetros antes de retornar ao usuário. Isso é necessário, pois envolvo o valor no qual clono, no mais simples functor, para poder modificá-lo sem violar a pureza das funções auxiliares. Aqui está a implementação deste functor:

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

Ele pode fazer duas coisas - map (se a função do mapeador é passada para ele) e extrair (se nada for passado).

Agora vamos analisar as funções auxiliares cloneObject, cloneFunction e clonePrimitive. Cada um deles pega 1 argumento da fonte de um tipo específico e retorna seu clone.

A implementação do cloneObject deve levar em consideração que as matrizes também são do tipo objeto, bem, em outros casos, elas devem clonar os campos e o protótipo. Aqui está a sua implementação:

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

A matriz pode ser copiada usando o método de fatia, mas como temos uma clonagem profunda e a matriz pode conter não apenas valores primitivos, o método map é usado com o deepClone descrito acima como argumento.

Para outros objetos, criamos um novo objeto e o envolvemos em nosso functor descrito acima, clonamos os campos (junto com os descritores) usando a função auxiliar cloneFields e depois clonamos o protótipo usando clonePrototype.

Funções auxiliares que descreverei abaixo. Enquanto isso, considere a implementação de cloneFunction :

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

Você simplesmente não pode clonar uma função com toda a lógica. Mas você pode envolvê-lo em outra função que chama o original com todos os argumentos e contexto e retorna seu resultado. Esse "clone" certamente manterá a função original na memória, mas "pesará" um pouco e reproduzirá totalmente a lógica original. Envolvemos a função clonada em um functor e, usando cloneFields, copiamos todos os campos da função original para ela, pois a função em JS também é um objeto, apenas chamado, e, portanto, pode armazenar campos nela.

Potencialmente, uma função pode ter um protótipo diferente de Function.prototype, mas não considerei esse caso extremo. Um dos encantos do FP é que podemos adicionar facilmente um novo invólucro a uma função existente para implementar a funcionalidade necessária.

O último bloco de construção clonePrimitive serve para clonar valores primitivos. Mas como os valores primitivos são copiados por valor (ou por referência, mas são imutáveis ​​em algumas implementações de mecanismos JS), podemos simplesmente copiá-los. Mas como não é esperado que obtenhamos um valor puro, mas um valor envolvido em um functor que extrair pode chamar sem argumentos, envolveremos nosso valor em uma função:

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

Agora, implementamos as funções auxiliares usadas acima - clonePrototype e cloneFields

Para clonar um protótipo, o clonePrototype simplesmente extrai o protótipo do objeto de origem e, executando uma operação de mapa no functor resultante, defina-o no objeto de destino:

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

A clonagem de campos é um pouco mais complicada, portanto, divido a função cloneFields em duas. A função externa pega a concatenação de todos os campos nomeados e todos os campos de símbolos, recebendo absolutamente todos os campos, e os executa pelo redutor criado pela função auxiliar:

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

O makeCloneFieldReducer deve criar uma função redutora para nós que possa ser passada ao método de redução em uma matriz de todos os campos do objeto de origem. Como bateria, nosso functor que armazena o alvo será usado. O redutor deve extrair o identificador do campo do objeto de origem e atribuí-lo ao campo do objeto de destino. Mas aqui é importante considerar que existem dois tipos de descritores - com valor e com get / set. Obviamente, o valor precisa ser clonado, mas com get / set não existe essa necessidade, um descritor pode ser retornado como é:

 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)); }; } 

Isso é tudo. Essa implementação do deepClone resolve todos os problemas colocados no início do artigo. Além disso, ele é construído sobre funções puras e um functor, que fornece todas as garantias inerentes ao cálculo lambda.

Também observo que não implementei um excelente comportamento para coleções que não sejam uma matriz que valeria a pena clonar individualmente, como Map ou Set. Embora em alguns casos isso possa ser necessário.

Source: https://habr.com/ru/post/pt466535/


All Articles