Copia profunda en JavaScript con estructuradoClone

La plataforma ahora se incluye constructuredClone(), una función integrada para la copia profunda.

Durante más tiempo, tuviste que recurrir a soluciones alternativas y bibliotecas para crear una copia profunda de un valor de JavaScript. La plataforma ahora se incluye con structuredClone(), una función integrada para la copia profunda.

Navegadores compatibles

  • Chrome: 98.
  • Borde: 98.
  • Firefox: 94.
  • Safari: 15.4.

Origen

Copias superficiales

Copiar un valor en JavaScript casi siempre es superficial, a diferencia de profundo. Esto significa que los cambios en los valores profundamente anidados serán visibles tanto en la copia como en el original.

Esta es una forma de crear una copia superficial en JavaScript con el operador de distribución de objetos ...:

const myOriginal = {
  someProp: "with a string value",
  anotherProp: {
    withAnotherProp: 1,
    andAnotherProp: true
  }
};

const myShallowCopy = {...myOriginal};

Agregar o cambiar una propiedad directamente en la copia superficial solo afectará a la copia, no al original:

myShallowCopy.aNewProp = "a new value";
console.log(myOriginal.aNewProp)
// ^ logs `undefined`

Sin embargo, agregar o cambiar una propiedad profundamente anidada afecta tanto la copia como la original:

myShallowCopy.anotherProp.aNewProp = "a new value";
console.log(myOriginal.anotherProp.aNewProp) 
// ^ logs `a new value`

La expresión {...myOriginal} itera en las propiedades (enumerables) de myOriginal con el operador de distribución. Usa el nombre y el valor de la propiedad, y los asigna uno por uno a un objeto vacío recién creado. Como tal, el objeto resultante tiene una forma idéntica, pero con su propia copia de la lista de propiedades y valores. Los valores también se copian, pero el valor de JavaScript maneja de manera diferente los llamados valores primitivos de manera diferente a los valores no primitivos. Para citar MDN, haz lo siguiente:

En JavaScript, un primitivo (valor primitivo, tipo de datos primitivo) son datos que no son un objeto y no tienen métodos. Existen siete tipos de datos primitivos: cadena, número, bigint, booleano, indefinido, símbolo y nulo.

MDN: Primitive

Los valores no primitivos se manejan como referencias, lo que significa que el acto de copiar el valor en realidad es copiar una referencia al mismo objeto subyacente, lo que da como resultado el comportamiento de copia superficial.

Copias profundas

Lo opuesto a una copia superficial es una copia profunda. Un algoritmo de copia profunda también copia las propiedades de un objeto una por una, pero se invoca de manera recursiva cuando encuentra una referencia a otro objeto, lo que también crea una copia de ese objeto. Esto puede ser muy importante para garantizar que dos fragmentos de código no compartan accidentalmente un objeto y manipulen sin saberlo el estado de los demás.

Antes no había una manera fácil o agradable de crear una copia profunda de un valor en JavaScript. Muchas personas dependían de bibliotecas de terceros, como la función cloneDeep() de Lodash. Podría decirse que la solución más común a este problema fue un hackeo basado en JSON:

const myDeepCopy = JSON.parse(JSON.stringify(myOriginal));

De hecho, esta era una solución alternativa tan popular, que V8 optimizó de forma agresiva JSON.parse() y, en particular, el patrón anterior para que fuera lo más rápido posible. Y, si bien es rápido, tiene algunas deficiencias y errores:

  • Estructuras de datos recursivas: JSON.stringify() arrojará cuando le asignes una estructura de datos recursiva. Esto puede suceder con bastante facilidad cuando se trabaja con listas o árboles vinculados.
  • Tipos integrados: JSON.stringify() arrojará si el valor contiene otros elementos integrados de JS, como Map, Set, Date, RegExp o ArrayBuffer.
  • Funciones: JSON.stringify() descartará las funciones de forma silenciosa.

Clonación estructurada

La plataforma ya necesitaba crear copias detalladas de los valores de JavaScript en varios lugares: almacenar un valor JS en IndexedDB requiere algún tipo de serialización para poder almacenarse en el disco y, luego, deserializarse para restablecer el valor de JS. De manera similar, para enviar mensajes a un WebWorker a través de postMessage(), es necesario transferir un valor de JS de un dominio de JS a otro. El algoritmo que se usa para esto se llama “Clonación estructurada” y, hasta hace poco, no era fácil de acceder para los desarrolladores.

Eso ha cambiado. Se modificó la especificación HTML para exponer una función llamada structuredClone() que ejecuta exactamente ese algoritmo como un medio para que los desarrolladores creen copias profundas de los valores de JavaScript con facilidad.

const myDeepCopy = structuredClone(myOriginal);

Eso es todo. Eso es toda la API. Si quieres obtener más detalles, consulta el artículo de MDN.

Funciones y limitaciones

La clonación estructurada resuelve muchas deficiencias (aunque no todas) de la técnica JSON.stringify(). La clonación estructurada puede controlar estructuras de datos cíclicas, admitir muchos tipos de datos integrados y, en general, es más sólida y, a menudo, más rápida.

Sin embargo, presenta algunas limitaciones que podrían tomarte por sorpresa:

  • Prototipos: Si usas structuredClone() con una instancia de clase, obtendrás un objeto plano como el resultado. valor, ya que la clonación estructurada descarta la cadena del prototipo del objeto.
  • Funciones: Si el objeto contiene funciones, structuredClone() arrojará una excepción DataCloneError.
  • No clonables: Algunos valores no son clonables estructurados, en particular los nodos Error y DOM. Integra hará que arroje structuredClone().

Si alguna de estas limitaciones no marca el acuerdo para tu caso de uso, las bibliotecas como Lodash también proporcionan implementaciones personalizadas de otros algoritmos de clonación profunda que pueden o no adaptarse a tu caso de uso.

Rendimiento

Si bien no hice una nueva comparación de microcomparativas, hice una a principios de 2018, antes de que se expusiera structuredClone(). En ese entonces, JSON.parse() era la opción más rápida para objetos muy pequeños. Espero que siga siendo igual. Las técnicas que se basaban en la clonación estructurada fueron (significativamente) más rápidas para objetos más grandes. Teniendo en cuenta que el nuevo structuredClone() viene sin la sobrecarga de abusar de otras APIs y es más sólido que JSON.parse(), te recomendamos que lo establezcas como tu enfoque predeterminado para crear copias profundas.

Conclusión

Si necesitas crear una copia profunda de un valor en JS, quizás porque usas estructuras de datos inmutables o quieres asegurarte de que una función pueda manipular un objeto sin afectar al original, ya no necesitas buscar soluciones alternativas ni bibliotecas. El ecosistema de JS ahora tiene structuredClone(). ¡Viva!