Como sabe en JavaScript, los objetos se copian por referencia. Pero a veces necesitas hacer una clonación profunda de un objeto. Muchas bibliotecas js ofrecen su implementación de la función deepClone para este caso. Pero, desafortunadamente, la mayoría de las bibliotecas no tienen en cuenta varias cosas importantes:
- Las matrices pueden estar en el objeto y es mejor copiarlas como matrices
- El objeto puede tener campos con un símbolo como clave
- Los campos de objeto tienen descriptores distintos al predeterminado
- Las funciones pueden estar en los campos del objeto y también deben ser clonadas.
- Un objeto finalmente tiene un prototipo diferente de Object.prototype
Quién lo rompió, coloqué el código completo debajo del spoilerfunction 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))); }
Mi implementación está escrita en un estilo funcional que me brinda confiabilidad, estabilidad y simplicidad. Pero dado que, desafortunadamente, muchos todavía no pueden reconstruir su pensamiento con procedimientos y pseudo-OOP, explicaré cada elemento básico de mi implementación:
La función deepClone tomará 1 fuente de argumento: la fuente desde la que clonaremos, y se devolverá su clon profundo con todas las características anteriores:
function deepClone(source) { return ({ 'object': cloneObject, 'function': cloneFunction }[typeof source] || clonePrimitive)(source)(); }
Aquí todo es simple, dependiendo del tipo de datos en la fuente, se selecciona una función que puede clonarla, y la fuente misma se transfiere a ella.
También puede observar que el resultado devuelto se llama como una función sin parámetros antes de ser devuelto al usuario. Esto es necesario, ya que envuelvo el valor en el que clono, en el functor más simple, para poder mutarlo sin violar la pureza de las funciones auxiliares. Aquí está la implementación de este functor:
function simpleFunctor(value) { return mapper => mapper ? simpleFunctor(mapper(value)) : value; }
Puede hacer 2 cosas: mapa (si se le pasa la función del mapeador) y extraer (si no se pasa nada).
Ahora analizaremos las funciones auxiliares cloneObject, cloneFunction y clonePrimitive. Cada uno de ellos toma 1 argumento de origen de un tipo específico y devuelve su clon.
La implementación de
cloneObject debe tener en cuenta que las matrices también son de tipo objeto, bueno, en otros casos, deben clonar los campos y el prototipo. Aquí está su implementación:
function cloneObject(source) { return (Array.isArray(source) ? () => source.map(deepClone) : clonePrototype(source, cloneFields(source, simpleFunctor({}))) ); }
La matriz se puede copiar utilizando el método de división, pero como tenemos una clonación profunda y la matriz puede contener no solo valores primitivos, el método de mapa se usa con el deepClone descrito anteriormente como argumento.
Para otros objetos, creamos un nuevo objeto y lo envolvemos en nuestro functor descrito anteriormente, clonamos los campos (junto con los descriptores) usando la función auxiliar cloneFields, y luego clonamos el prototipo usando clonePrototype.
Funciones de ayuda que describiré a continuación. Mientras tanto, considere la implementación de
cloneFunction :
function cloneFunction(source) { return cloneFields(source, simpleFunctor(function() { return source.apply(this, arguments); })); }
Simplemente no puede clonar una función con toda la lógica. Pero puede envolverlo en otra función que llame al original con todos los argumentos y contexto, y devuelva su resultado. Tal "clon" ciertamente mantendrá la función original en la memoria, pero "pesará" un poco y reproducirá completamente la lógica original. Envolvemos la función clonada en un functor y usando cloneFields copiamos todos los campos de la función original, ya que la función en JS también es un objeto, simplemente llamado, y por lo tanto puede almacenar campos en él.
Potencialmente, una función puede tener un prototipo diferente de Function.prototype, pero no consideré este caso extremo. Uno de los encantos de FP es que podemos agregar fácilmente un nuevo contenedor sobre una función existente para implementar la funcionalidad necesaria.
El último ladrillo de construcción clonePrimitive sirve para clonar valores primitivos. Pero dado que los valores primitivos se copian por valor (o por referencia, pero son inmutables en algunas implementaciones de motores JS), simplemente podemos copiarlos. Pero como no se espera que obtengamos un valor puro, sino un valor envuelto en un functor que extract puede llamar sin argumentos, envolveremos nuestro valor en una función:
function clonePrimitive(source) { return () => source; }
Ahora implementamos las funciones auxiliares que se usaron anteriormente: clonePrototype y cloneFields
Para clonar un prototipo,
clonePrototype simplemente extraerá el prototipo del objeto de origen y, al realizar una operación de mapa en el functor resultante, lo establecerá en el objeto de destino:
function clonePrototype(source, destinationFunctor) { return destinationFunctor(destination => Object.setPrototypeOf(destination, Object.getPrototypeOf(source))); }
Clonar campos es un poco más complicado, así que
dividí la función
cloneFields en dos. La función externa toma la concatenación de todos los campos con nombre y todos los campos de símbolos, recibe absolutamente todos los campos, y los ejecuta a través del reductor creado por la función auxiliar:
function cloneFields(source, destinationFunctor) { return (Object.getOwnPropertyNames(source) .concat(Object.getOwnPropertySymbols(source)) .reduce(makeCloneFieldReducer(source), destinationFunctor) ); }
makeCloneFieldReducer debería crear una función reductora para nosotros que podría pasarse al método reduce en una matriz de todos los campos del objeto fuente. Como batería, se utilizará nuestro functor que almacena el objetivo. El reductor debe extraer el identificador del campo del objeto de origen y asignarlo al campo del objeto de destino. Pero aquí es importante tener en cuenta que hay dos tipos de descriptores: con valor y con get / set. Obviamente, el valor necesita ser clonado, pero con get / set no existe tal necesidad, un descriptor puede ser devuelto tal como está:
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)); }; }
Eso es todo Tal implementación de deepClone resuelve todos los problemas planteados al comienzo del artículo. Además, se basa en funciones puras y un solo functor, lo que brinda todas las garantías inherentes al cálculo lambda.
También noto que no implementé un comportamiento excelente para colecciones que no sean una matriz que valdría la pena clonar individualmente, como Map o Set. Aunque en algunos casos esto puede ser necesario.