Como fazer cópia profunda em JavaScript usando oStructuredClone

A plataforma agora vem com structuredClone(), uma função integrada para cópia profunda.

Durante o maior tempo, você precisou recorrer a soluções alternativas e bibliotecas para criar uma cópia detalhada de um valor JavaScript. A plataforma agora vem com structuredClone(), uma função integrada para cópia profunda.

Compatibilidade com navegadores

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

Origem

A cópia de um valor em JavaScript quase sempre é superficial, e não profunda. Isso significa que as mudanças em valores profundamente aninhados vão ficar visíveis na cópia e no original.

Uma maneira de criar uma cópia superficial em JavaScript usando o operador de propagação de objeto ...:

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

const myShallowCopy = {...myOriginal};

Adicionar ou alterar uma propriedade diretamente na cópia superficial afetará apenas a cópia, não o original:

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

No entanto, adicionar ou mudar uma propriedade profundamente aninhada afeta ambos, a cópia e o original:

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

A expressão {...myOriginal} itera sobre as propriedades (enumeráveis) de myOriginal usando o operador de propagação. Ele usa o nome e o valor da propriedade e os atribui um por um a um objeto vazio recém-criado. Assim, o objeto resultante tem a mesma forma, mas com uma cópia própria da lista de propriedades e valores. Os valores também são copiados, mas os chamados valores primitivos são tratados de maneira diferente pelo valor JavaScript do que os valores não primitivos. Citando o MDN:

Em JavaScript, um tipo primitivo (valor primitivo, tipo de dados primitivo) é um dado que não é um objeto e não tem métodos. Há sete tipos de dados primitivos: string, number, bigint, booleano, indefinido, símbolo e nulo.

MDN — Primitivo

Valores não primitivos são processados como referências, o que significa que o ato de copiar o valor é apenas copiar uma referência para o mesmo objeto, resultando no comportamento de cópia superficial.

Cópias profundas

O oposto de uma cópia superficial é uma cópia detalhada. Um algoritmo de cópia detalhada também copia as propriedades de um objeto uma por uma, mas se invoca recursivamente quando encontra uma referência a outro objeto, criando uma cópia desse objeto também. Isso pode ser muito importante para garantir que dois códigos não compartilhem um objeto acidentalmente e manipulem o estado um do outro sem saber.

Não havia uma maneira fácil ou agradável de criar uma cópia detalhada de um valor em JavaScript. Muitas pessoas dependiam de bibliotecas de terceiros, como a função cloneDeep() do Lodash. Pode-se argumentar que a solução mais comum para esse problema foi uma invasão baseada em JSON:

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

Na verdade, essa era uma solução alternativa tão popular que o V8 foi otimizado agressivamente JSON.parse() e, especificamente, o padrão acima foi otimizado para ser o mais rápido possível. Embora seja rápido, ele tem algumas falhas e armadilhas:

  • Estruturas de dados recursivas: JSON.stringify() será gerado quando você fornecer uma estrutura de dados recursiva. Isso pode acontecer com facilidade ao trabalhar com listas ou árvores vinculadas.
  • Tipos integrados: JSON.stringify() será gerado se o valor contiver outros JS integrados, como Map, Set, Date, RegExp ou ArrayBuffer.
  • Funções: JSON.stringify() descarta funções silenciosamente.

Clonagem estruturada

A plataforma já precisava da capacidade de criar cópias profundas de valores JavaScript em alguns lugares: armazenar um valor JS no IndexedDB requer alguma forma de serialização para que ele possa ser armazenado no disco e desserializado posteriormente para restaurar o valor JS. Da mesma forma, enviar mensagens para um WebWorker por postMessage() exige a transferência de um valor de JS de um domínio para outro. O algoritmo usado para isso é chamado de "Clone estruturado" e, até recentemente, não era facilmente acessível aos desenvolvedores.

Isso mudou! A especificação HTML foi alterada para expor uma função chamada structuredClone(), que executa exatamente esse algoritmo como uma forma de os desenvolvedores criarem cópias profundas de valores JavaScript.

const myDeepCopy = structuredClone(myOriginal);

Pronto. Essa é a API completa. Se você quiser saber mais detalhes, consulte este artigo sobre MDN.

Recursos e limitações

A clonagem estruturada resolve muitas deficiências (mas não todas) da técnica JSON.stringify(). A clonagem estruturada pode processar estruturas de dados cíclicas, oferecer suporte a muitos tipos de dados integrados e geralmente é mais robusta e rápida.

No entanto, ele ainda tem algumas limitações que podem pegar você de surpresa:

  • Prototipos: se você usar structuredClone() com uma instância de classe, receberá um objeto simples como o valor de retorno, já que a clonagem estruturada descarta a cadeia de protótipos do objeto.
  • Funções: se o objeto tiver funções, structuredClone() vai gerar uma exceção DataCloneError.
  • Não clonáveis: alguns valores não são estruturados de forma clonável, principalmente Error e nós DOM. Isso fará com que structuredClone() seja gerado.

Se alguma dessas limitações for um obstáculo para seu caso de uso, bibliotecas como a Lodash ainda fornecem implementações personalizadas de outros algoritmos de clonagem profunda que podem ou não se adequar ao seu caso de uso.

Desempenho

Embora eu não tenha feito uma nova comparação de microcomparação, faça uma comparação no início de 2018, antes da exposição do structuredClone(). Naquela época, JSON.parse() era a opção mais rápida para objetos muito pequenos. Espero que continue a mesma. As técnicas que dependiam da clonagem estruturada eram (significativamente) mais rápidas para objetos maiores. Considerando que o novo structuredClone() vem sem a sobrecarga do abuso de outras APIs e é mais robusto que o JSON.parse(), recomendamos que você torne essa a abordagem padrão para criar cópias profundas.

Conclusão

Se você precisar criar uma cópia profunda de um valor em JS, talvez porque use estruturas de dados imutáveis ou queira garantir que uma função possa manipular um objeto sem afetar o original, não será mais necessário recorrer a soluções alternativas ou bibliotecas. O ecossistema do JS agora tem structuredClone(). Irradiando.