Como fazer cópia profunda em JavaScript usando oStructuredClone

Agora a plataforma vem comstructuredClone(), 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 é fornecida com structuredClone(), uma função integrada para cópia profunda.

Compatibilidade com navegadores

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

Origem

Cópias superficiais

A cópia de um valor no JavaScript é quase sempre superficial, ao contrário de deep. Isso significa que as mudanças em valores aninhados em muitos níveis serão visíveis na cópia e no original.

Uma forma 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 alterar uma propriedade aninhada em vários níveis afeta a cópia e a original:

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

A expressão {...myOriginal} faz a iteração das propriedades (enumeráveis) de myOriginal usando o operador Spread. Ela 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 é idêntico em forma, mas com sua própria cópia 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 em comparação com os valores não primitivos. Para citar o MDN:

Em JavaScript, um primitivo (valor primitivo, tipo de dados primitivo) são dados que não são um objeto e não têm 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 tratados como referências, o que significa que o ato de copiar o valor é, na verdade, apenas copiar uma referência para o mesmo objeto subjacente, resultando em um comportamento de cópia superficial.

Cópias avançadas

O oposto de uma cópia superficial é uma cópia profunda. Um algoritmo de cópia profunda também copia as propriedades de um objeto, uma por uma, mas invoca a si mesmo 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 acidentalmente um objeto 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() da Lodash. Provavelmente, 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 conhecida que o V8 otimizou JSON.parse() de forma agressiva e especificamente o padrão acima para torná-la o mais rápida possível. Embora seja rápido, ele tem algumas desvantagens e pontos fracos:

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

Clonagem estruturada

A plataforma já precisava da capacidade de criar cópias profundas de valores JavaScript em alguns locais: armazenar um valor JS no IndexedDB requer alguma forma de serialização para que ele possa ser armazenado em disco e depois desserializado para restaurar o valor JS. Da mesma forma, enviar mensagens a um WebWorker via postMessage() requer a transferência de um valor de JS de um realm 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 do HTML foi alterada para expor uma função chamada structuredClone(), que executa exatamente esse algoritmo como um meio para os desenvolvedores criarem cópias detalhadas de valores JavaScript com facilidade.

const myDeepCopy = structuredClone(myOriginal);

Pronto. Essa é a API inteira. 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 lidar com estruturas cíclicas de dados, oferecer suporte a muitos tipos de dados integrados e geralmente é mais robusta e geralmente mais rápida.

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

  • Protótipos: se você usar structuredClone() com uma instância de classe, vai receber um objeto simples como retorno valor, já que a clonagem estruturada descarta a cadeia do protótipo do objeto.
  • Funções: se o objeto contiver funções, structuredClone() vai gerar uma exceção DataCloneError.
  • Não clonáveis: alguns valores não são clonáveis estruturados, principalmente os nós Error e DOM. Ela vai fazer 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 de 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 detalhada 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 buscar soluções alternativas ou bibliotecas. O ecossistema JS agora tem structuredClone(). Uzzah.