Copia profunda en JavaScript con estructuradoClone

La plataforma ahora se envía con structuredClone(), una función integrada para la copia profunda.

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

Navegadores compatibles

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

Origen

Copiar un valor en JavaScript casi siempre es superficial, en lugar de profundo. Esto significa que los cambios en los valores anidados de forma profunda se verán en la copia y en el original.

Una forma de crear una copia superficial en JavaScript con el operador de expansión de objetos ...:

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

const myShallowCopy = {...myOriginal};

Si agregas o cambias una propiedad directamente en la copia superficial, solo se verá afectada la copia, no la original:

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

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

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

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

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

MDN: primitivo

Los valores no primitivos se manejan como referencias, lo que significa que el acto de copiar el valor en realidad solo copia una referencia al mismo objeto subyacente, lo que genera 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 forma recursiva cuando encuentra una referencia a otro objeto y también crea una copia de ese objeto. Esto puede ser muy importante para asegurarse de que dos fragmentos de código no compartan accidentalmente un objeto y manipulen el estado de cada uno sin saberlo.

Antes no había una forma 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. Se podría decir que la solución más común a este problema fue un hack basado en JSON:

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

De hecho, esta fue una solución alternativa tan popular que V8 optimizó de forma agresiva JSON.parse() y, específicamente, el patrón anterior para que fuera lo más rápido posible. Si bien es rápido, tiene algunas deficiencias y trampas:

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

Clonación estructurada

La plataforma ya necesitaba la capacidad de crear copias profundas de valores de JavaScript en un par de lugares: almacenar un valor de JS en IndexedDB requiere algún tipo de serialización para que se pueda almacenar en el disco y, luego, serializarlo para restablecer el valor de JS. Del mismo modo, enviar mensajes a un WebWorker a través de postMessage() requiere transferir un valor de JS de un reino de JS a otro. El algoritmo que se usa para esto se denomina "Clone estructurado" y, hasta hace poco, los desarrolladores no podían acceder a él fácilmente.

Eso cambió. 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 valores de JavaScript con facilidad.

const myDeepCopy = structuredClone(myOriginal);

Eso es todo. Esa es toda la API. Si quieres profundizar en los detalles, consulta el artículo de MDN.

Funciones y limitaciones

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

Sin embargo, aún tiene algunas limitaciones que pueden sorprenderte:

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

Si alguna de estas limitaciones es un factor decisivo para tu caso de uso, bibliotecas como Lodash aú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, realicé una comparación a principios de 2018, antes de que se expusiera structuredClone(). En ese momento, JSON.parse() era la opción más rápida para objetos muy pequeños. Espero que eso siga igual. Las técnicas que se basaban en la clonación estructurada eran (significativamente) más rápidas para objetos más grandes. Dado que el nuevo structuredClone() no tiene la sobrecarga de abusar de otras APIs y es más sólido que JSON.parse(), te recomiendo que lo conviertas en 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(). ¡Hurra!