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, era preciso 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.

Browser Support

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

Source

Cópias superficiais

Copiar um valor em JavaScript é quase sempre superficial, em vez de profundo. Isso significa que as mudanças em valores profundamente aninhados também 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 tanto a cópia quanto 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 é idêntico em 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 forma diferente pelo valor do JavaScript em comparação com os valores não primitivos. Para citar o MDN:

Em JavaScript, um primitivo (valor primitivo, tipo de dado primitivo) é um dado que não é um objeto e não tem métodos. Há sete tipos de dados primitivos: string, number, bigint, boolean, undefined, symbol e null.

MDN — Primitive

Os 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 ao mesmo objeto, resultando no comportamento de cópia superficial.

Cópias profundas

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 se invoca de forma recursiva quando encontra uma referência a outro objeto, criando uma cópia desse objeto também. Isso pode ser muito importante para garantir que duas partes do código não compartilhem acidentalmente um objeto e manipulem o estado uma da outra sem saber.

Antes, não havia uma maneira fácil ou agradável 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. Provavelmente, a solução mais comum para esse problema era 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 otimizou agressivamente o JSON.parse() e, especificamente, o padrão acima para torná-lo o mais rápido possível. Embora seja rápido, ele tem algumas deficiências e armadilhas:

  • Estruturas de dados recursivas: o JSON.stringify() vai gerar um erro quando você fornecer uma estrutura de dados recursiva. Isso pode acontecer facilmente ao trabalhar com listas ou árvores vinculadas.
  • Tipos integrados: JSON.stringify() vai gerar um erro se o valor tiver outros elementos integrados do JS, como Map, Set, Date, RegExp ou ArrayBuffer.
  • Funções: JSON.stringify() vai descartar 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 depois desserializado para restaurar o valor JS. Da mesma forma, o envio de mensagens para um WebWorker via postMessage() requer a transferência de um valor 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 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 facilmente cópias profundas de valores JavaScript.

const myDeepCopy = structuredClone(myOriginal);

Pronto. Essa é toda a API. Se quiser saber mais detalhes, confira o artigo da 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 lidar com estruturas de dados cíclicas, é compatível com 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:

  • Protótipos: se você usar structuredClone() com uma instância de classe, vai receber um objeto simples como 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 clonáveis de forma estruturada, principalmente Error e nós do DOM. Isso vai fazer com que structuredClone() seja gerado.

Se alguma dessas limitações for um problema para seu caso de uso, bibliotecas como 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 microcomparativo de mercado, fiz uma no início de 2018, antes de structuredClone() ser exposto. Na época, o JSON.parse() era a opção mais rápida para objetos muito pequenos. Acredito que isso não vai mudar. As técnicas que dependiam da clonagem estruturada eram (significativamente) mais rápidas para objetos maiores. Considerando que o novo structuredClone() não tem a sobrecarga de abusar de outras APIs e é mais robusto do que JSON.parse(), recomendo que você o use como 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 usar soluções alternativas ou bibliotecas. O ecossistema JS agora tem structuredClone(). Viva!