Como fazer cópia profunda em JavaScript usando oStructuredClone

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

Por muito tempo, você teve que recorrer a soluções alternativas e bibliotecas para criar uma cópia profunda 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

Cópias superficiais

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 mudar uma propriedade diretamente na cópia superficial afeta 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 processados de maneira diferente pelo valor JavaScript em comparação com os valores não primitivos. Para citar 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, número, 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 legal de criar uma cópia profunda de um valor em JavaScript. Muitas pessoas dependiam de bibliotecas de terceiros, como a função cloneDeep() do Lodash. A solução mais comum para esse problema foi um hack baseado 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 para torná-lo 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 tipos integrados do JS, 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 de JS para outro. O algoritmo usado para isso é chamado de "Clone estruturado" e, até recentemente, não era facilmente acessível para os 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 quiser saber mais detalhes, consulte o artigo do MDN.

Recursos e limitações

A clonagem estruturada resolve muitas (mas não todas) as deficiências 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, ainda há algumas limitações que podem surpreender você:

  • 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 vai fazer com que structuredClone() seja lançado.

Se alguma dessas limitações for um problema para seu caso de uso, bibliotecas como o Lodash ainda oferecem 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 microbenchmark, fiz uma comparação no início de 2018, antes que o structuredClone() fosse exposto. Naquela época, JSON.parse() era a opção mais rápida para objetos muito pequenos. Espero que isso continue igual. As técnicas que dependiam da clonagem estruturada eram (significativamente) mais rápidas para objetos maiores. Considerando que a nova structuredClone() não tem a sobrecarga de abusar de outras APIs e é mais robusta do que JSON.parse(), recomendo que você a use como sua 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.