Como fazer cópia profunda em JavaScript usando oStructuredClone

A plataforma agora tem oStructuredClone(), uma função integrada para cópia profunda.

Por mais tempo, foi preciso 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

  • 98
  • 98
  • 94
  • 15,4

Origem

Cópias superficiais

Copiar um valor em JavaScript é quase sempre superficial, ao contrário de profundo. Isso significa que as mudanças nos valores aninhados em níveis profundos ficarão 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 só afetará 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 camadas afeta tanto a cópia quanto a original:

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

A expressão {...myOriginal} itera sobre as propriedades (enumerais) de myOriginal usando o operador Spread. Ela usa o nome e o valor da propriedade e os atribui um a um a um objeto vazio recém-criado. Assim, o objeto resultante tem forma idêntica, mas com a própria cópia da lista de propriedades e valores. Os valores também são copiados, mas os valores assim chamados primitivos são tratados de forma diferente pelo valor JavaScript do que valores não primitivos. Para citar 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, número, bigint, booleano, indefinido, símbolo e nulo.

MDN: primitivo

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

Cópias detalhadas

O oposto de uma cópia superficial é a cópia profunda. Um algoritmo de cópia detalhada também copia as propriedades de um objeto uma por uma, mas invoca-se 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 duas partes de código não compartilhem acidentalmente um objeto e manipulem sem saber o estado uma da outra.

Não havia uma maneira fácil ou boa 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 ser que a solução mais comum para esse problema tenha sido uma invasão baseada em JSON:

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

Na verdade, essa era uma solução tão conhecida que o V8 otimizou de forma agressiva JSON.parse() e, especificamente, o padrão acima para torná-lo o mais rápido possível. Embora seja rápido, ele apresenta algumas deficiências e trilhos:

  • Estruturas de dados recursivas: o JSON.stringify() é gerado quando você atribui uma estrutura de dados recursiva. Isso pode acontecer facilmente ao trabalhar com árvores ou listas vinculadas.
  • Tipos integrados: o JSON.stringify() vai ser gerado se o valor tiver outros componentes integrados do JS, como Map, Set, Date, RegExp ou ArrayBuffer.
  • Funções: 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: o armazenamento de um valor JS no IndexedDB requer alguma forma de serialização para que possa ser armazenado em disco e posteriormente desserializado para restaurar o valor de JS. Da mesma forma, o envio de mensagens a um WebWorker via postMessage() exige a transferência de um valor de JS de um realm 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 um meio para os desenvolvedores criarem facilmente cópias profundas de valores JavaScript.

const myDeepCopy = structuredClone(myOriginal);

Pronto! Essa é a API completa. Se você quiser saber mais detalhes, consulte o artigo da MDN.

Recursos e limitações

A clonagem estruturada resolve muitas (embora não todas) deficiências 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, muitas vezes, mais rápida.

No entanto, ela 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, o structuredClone() vai gerar uma exceção DataCloneError.
  • Não clonáveis: alguns valores não são clonáveis estruturados, principalmente Error e nós DOM. Isso fará com que structuredClone() seja gerado.

Mesmo que qualquer uma dessas limitações possa prejudicar seu caso de uso, bibliotecas como a Lodash ainda vão oferecer implementações personalizadas de outros algoritmos de clonagem profunda que podem ou não se adequar ao seu caso de uso.

Desempenho

Ainda não fiz uma nova comparação de microcomparações, mas fiz uma comparação no início de 2018, antes de o structuredClone() ser exposto. Naquela época, JSON.parse() era a opção mais rápida para objetos muito pequenos. Espero que permaneça o mesmo. 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 uso de outras APIs e é mais robusto do que o JSON.parse(), recomendo que você o torne sua abordagem padrão para a criação de cópias detalhadas.

Conclusão

Se você precisar criar uma cópia detalhada de um valor em JS, talvez seja porque você usa estruturas de dados imutáveis ou quer garantir que uma função possa manipular um objeto sem afetar o original, não é mais necessário procurar soluções alternativas ou bibliotecas. O ecossistema JS agora tem structuredClone(). Irrá.