Cópias de objetos de forma eficiente com o StructuredClone

Entenda melhor como o JavaScript lida com cópias e serialização de objetos.

Atualizado em

Por muito tempo, desenvolvedores utilizaram hacks e bibliotecas para criar deep clone de objetos no JavaScript, no entanto, hoje não é mais necessário pois a função nativa structuredClone() veio para resolver esse problema. Mas antes de conhecer melhor essa alternativa, vamos entender melhor o problema.

Shallow copy

De modo geral, ao realizar uma cópia de um objeto, é feito uma shallow copy, ou seja, uma cópia superficial. Isso significa que, valores e propriedades aninhadas profundamente compartilham a mesma referência, apontando para os mesmos valores. Como resultado, quando uma propriedade é alterada na origem ou na cópia, o valor em ambos objetos será alterado, o que pode causar efeito colateral uma vez que possibilita uma alteração não intencional.

Uma forma comum de se criar shallow copy é utilizando a spread syntax:

const order = {
	id: '89e79180-4cf5-46f8-8a93-b386b017f8a6',
	createdAt: new Date('2022-03-03'),
	customer: {
		id: 'b5dd8c55-867c-49e7-ba20-a1ce5e647b31',
		name: 'John Doe',
	},
	products: ['T-Shirt'],
}
 
const orderCopy = { ...order }

Ao tentar alterar uma propriedade aninhada, ambos objetos são alterados:

orderCopy.customer.name = 'Jane Doe'
 
console.log(orderCopy.customer.name)
// Output: `Jane Doe`
 
console.log(order.customer.name)
// Output: `Jane Doe`

O mesmo ocorre com propriedades do tipo Date e Array:

orderCopy.createdAt = new Date('1975-08-19')
orderCopy.products.push('Shoes')
 
// Alteramos os valores de `createdAt` e `products` em ambos objetos.

Porém, a cópia superficial não é uma exclusividade do spread operator. As funções padrões para cópias de objetos como Array.prototype.concat(), Array.prototype.slice(), Array.from(), Object.assign(), e Object.create() também resultam em shallow copies.

Deep Copy

Por outro lado, é possível criar uma deep copy, ou cópias profundas, cujos valores e propriedades aninhadas profundamente não compartilham a mesma referência. Contudo, houve um tempo em que não havia uma forma simplificada de criar uma cópia profunda de objetos com JavaScript. Alguns desenvolvedores utilizavam biblioteca de terceiros como o cloneDeep() do Lodash. A outra alternativa era um truque baseado em JSON:

const deepCopy = JSON.parse(JSON.stringify(originalObject))

No entanto essa última solução apresenta alguns problemas:

  • A função JSON.stringify() lança uma exceção com a mensagem TypeError: cyclic object value caso seja utilizada uma uma estrutura de dados recursiva como uma circular linked list.

  • A função JSON.stringify() descarta valores como undefined, Function, Symbol uma vez que não são valores válidos para JSON.

  • A função JSON.stringify() serializa apenas propriedades próprias enumeráveis. Isso significa que Map, Set e etc se tornarão {}.

  • Números como Infinity e NaN, assim como null são considerados null.

  • Objetos do tipo Date são serializados para uma representação no formato de string, como 1975-08-19T23:15:30.000Z.

Sendo assim, a cópia de alguns objetos pode resultar em algo não desejado:

const foo = {
	map: new Map([[1, 2]]),
	set: new Set([1, 3, 3, 4]),
	regex: /ab+c/i,
	number: Infinity,
	date: new Date('1975-08-19'),
	function: (a, b) => a * b,
}
 
console.log(JSON.parse(JSON.stringify(foo)))
// Output:
// {
//   map: {},
//   set: {},
//   regex: {},
//   number: null,
//   date: '1975-08-19T00:00:00.000Z'
// }

Structured clone

O Structured clone é um algoritmo que cria cópias de objetos complexos em JavaScript. Ele é extremamente útil quando se precisa transferir dados entre Workers por meio do postMessage(), salvar objetos com IndexedDB ou copiar objetos para outras APIs. Ele clona de forma recursiva através do objeto de entrada, enquanto mantém um mapa das referências já visitadas, para evitar que percorra ciclos infinitamente.

Esse algoritmo é utilizado internamente quando se utiliza a função structuredClone() disponível nos navegadores e no Node.js a partir da versão 17. Sendo assim, é possível nativamente clonar objetos complexos como:

const foo = {
	map: new Map([[1, 2]]),
	set: new Set([1, 3, 3, 4]),
	regex: /ab+c/i,
	number: Infinity,
	date: new Date('1975-08-19'),
	function: (a, b) => a * b,
}
 
console.log(structuredClone(foo))
// Output:
// {
//   map: Map(1) { 1 => 2 },
//   set: Set(3) { 1, 3, 4 },
//   regex: /ab+c/i,
//   number: Infinity,
//   date: 1975-08-19T00:00:00.000Z
// }

Apesar de se tornar uma excelente alternativa para realizar deep copy de objetos, o structuredClone() possui algumas limitações:

  • Caso esteja copiando uma instância de uma classe, será retornado um objeto simples contendo apenas seus valores uma vez que o algoritmo descarta o prototype chain do objeto.

  • Function não são duplicadas pelo algoritmo lançando uma exceção do tipoDataCloneError.

  • Nós do DOM e instâncias de Error também lançam uma exceção do tipoDataCloneError.

Conclusão

Caso seja necessário realizar uma cópia profunda de um objeto, o structureClone() é uma ótima alternativa para a maioria dos casos. Contudo, o algoritmo structured clone ainda possui algumas limitações. Sendo assim, bibliotecas de terceiro, como o Lodash, podem ser uma alternativa ao provê uma implementação própria para deep copy que pode ou não atender ao seu caso.