Copia profunda en JavaScript con estructuradoClone

La plataforma ahora se envía con estructuradoClone(), 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 envía con structuredClone(), una función integrada para la copia profunda.

Navegadores compatibles

  • 98
  • 98
  • 94
  • 15.4

Origen

Copias superficiales

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

Esta es 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};

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

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

Sin embargo, agregar o cambiar una propiedad profundamente 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 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 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 maneja los llamados valores primitivos de manera diferente a los valores que no son primitivos. Para citar MDN:

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

MDN: Primitive

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

Copias profundas

Lo contrario 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 recurrente cuando encuentra una referencia a otro objeto y también crea una copia de ese objeto. Esto puede ser muy importante para garantizar que dos fragmentos de código no compartan un objeto por accidente y manipulen inadvertidamente el estado de cada uno.

Antes no existía una forma fácil ni 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 fue una solución alternativa tan popular que V8 optimizó considerablemente JSON.parse() y, en particular, el patrón anterior para que fuera lo más rápido posible. Si bien es rápido, presenta algunas deficiencias y trampas:

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

Clonación estructurada

La plataforma ya necesitaba la capacidad de crear copias profundas de valores JavaScript en algunos lugares: almacenar un valor JS en IndexedDB requiere alguna forma de serialización para que pueda almacenarse en el disco y luego deserializarse para restablecer el valor JS. Del mismo modo, 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 de fácil acceso para los desarrolladores.

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 fácilmente copias profundas de los valores de JavaScript.

const myDeepCopy = structuredClone(myOriginal);

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

Funciones y limitaciones

La clonación estructurada soluciona muchas (aunque no todas) las deficiencias de la técnica JSON.stringify(). La clonación estructurada puede manejar estructuras de datos cíclicos, admitir muchos tipos de datos integrados y, en general, es más sólida y, con frecuencia, más rápida.

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

  • Prototipos: Si usas structuredClone() con una instancia de clase, obtendrás un objeto sin formato como valor de retorno, ya que la clonación estructurada descarta la cadena del prototipo del objeto.
  • Funciones: Si tu objeto contiene funciones, structuredClone() arrojará una excepción DataCloneError.
  • No se pueden clonar: Algunos valores no se pueden clonar de forma estructurada, en particular los nodos Error y DOM. Eso provocará que se arroje structuredClone().

Si alguna de estas limitaciones es un factor decisivo para tu caso de uso, las 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 realicé una nueva comparación de microcomparativas, la realicé 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 el mismo. Las técnicas que se basaban en la clonación estructurada fueron (significativamente) más rápidas para los objetos más grandes. Teniendo en cuenta que el nuevo structuredClone() no tiene 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, tal vez porque usas estructuras de datos inmutables o quieres asegurarte de que una función pueda manipular un objeto sin afectar el original, ya no necesitas buscar soluciones alternativas ni bibliotecas. El ecosistema de JS ahora tiene structuredClone(). Huzzah.