Clonando objetos em JavaScript - Shallow vs Deep
Clonar objetos é uma tarefa comum para desenvolvedores JavaScript. A clonagem nos permite criar versões independentes e isoladas, preservando a integridade dos dados e evitando efeitos colaterais indesejados. Essa prática é especialmente útil em situações em que desejamos modificar apenas uma cópia do objeto, enquanto mantemos o objeto original intacto.
Além disso, em casos de gerenciamento de estado, como no Redux ou NGRX, é comum lidar com objetos imutáveis. Nestes casos, clonar o objeto se torna necessário para realizar modificações específicas, entregando um novo objeto como resultado do processo.
Estudando os métodos de clonagem de objetos em JavaScript podemos evitar problemas como no exemplo abaixo:
const obj = {
startDate: new Date("2023-01-01T12:00:00.000Z")
};
const clone = Object.assign(obj, {});
// Somando 5 minutos no clone.startDate
const fiveMinInMS = 5 * 60 * 1e3;
clone.startDate.setTime(
clone.startDate.getTime() + fiveMinInMS
);
// "2023-01-01T12:05:00.000Z"
console.log(clone.startDate.toISOString());
// O obj.startDate === clone.startDate
// "2023-01-01T12:05:00.000Z"
console.log(obj.startDate.toISOString());
Há diversas abordagens para clonar objetos em JavaScript, tais como usar Object.assign()
(exemplo acima) ou o spread operator (...
) para efetuar uma shallow copy (cópia "superficial"). Também podemos recorrer a função nativa structuredClone()
ou bibliotecas como lodash.cloneDeep()
para efetuar uma deep copy (cópia "profunda"). A escolha da abordagem depende das necessidades específicas de cada cenário e é isso que vamos explorar neste texto.
Shallow Copy
Quando utilizamos o ...
(spread operator) ou o Object.assign()
, estamos efetuando uma shallow copy de um objeto. Isso significa que estamos criando um novo objeto com uma nova referência. Mas é importante destacar alguns detalhes:
- as propriedades do tipo primitivo (primitive type), como por exemplo
string
enumber
, são copiadas gerando um novo valor, ocupando um novo espaço de memória, ou seja, são cópias independentes. - As propriedades do tipo referência (reference type), como arrays e objetos, tem suas referências copiadas, ou seja, não é feita uma cópia do objeto em si, mas sim do endereço de memória onde o objeto está armazenado. Portanto, as propriedades reference type do clone apontarão para o mesmo endereço das propriedades do objeto original. Podemos visualizar esse comportamento no exemplo a seguir:
const obj = {
nome: 'consolelog.com.br',
tags: [1, 2, 3],
objs: {
item1: 'valor1',
item2: 'valor2'
}
};
// Utilizando o spread operator (...)
// para clonar o objeto
const clone = {...obj};
// ou poderíamos utilizar o Object.assign:
// const clone = Object.assign({}, obj);
console.log(
typeof obj, // object (reference type)
typeof obj.nome, // string (primitive type)
typeof obj.tags, // object (reference type)
typeof obj.objs, // object (reference type)
);
console.log(
obj === clone, // false
obj.nome === clone.nome, // true
obj.tags === clone.tags, // true
obj.objs === clone.objs, // true
);
Analisando o código acima:
- a variável
obj
tem um campo primitivo (nome
) e dois referenciados (tags
eobjs
) clone
foi gerado a partir doobj
utilizando o spread operator, portanto podemos afirmar queobj !== clone
- O campos
tags
eobjs
das váriaveisobj
eclone
apontam para o mesmo objeto. Isso é um aspecto crucial a ser destacado, pois as cópias foram feitas por referência. Portanto, se fizermos uma alteração emobj.tags
, o resultado será refletido emclone.tags
, uma vez que ambos apontam para o mesmo local na memória. Por essa razão, se adicionarmos um novo item utilizandoclone.tags.push(4)
, o mesmo valor será adicionado aobj.tags
.
clone.tags.push(4);
// [1, 2, 3, 4]
console.log(clone.tags);
// [1, 2, 3, 4] (4)
console.log(obj.tags);
O mesmo exemplo acima se aplica a obj.objs
ou clone.objs
.
Um outro cenário comum é quando o objeto clonado tem algum campo do tipo Date
, veja no exemplo abaixo:
const obj = {
data: new Date("2023-08-01T12:00:00.000-0300")
};
const clone = Object.assign({}, obj);
// Adiciona 10 minutos
const dezMinutos = 10 * 60 * 1e3;
clone.data.setTime(clone.data.getTime() + dezMinutos);
console.log(clone.data);
console.log(obj.data);
// Output:
Tue Aug 01 2023 12:10:00 GMT-0300 (Brasilia Standard Time) (7.16.0.min.js, line 2)
Tue Aug 01 2023 12:10:00 GMT-0300 (Brasilia Standard Time) (7.16.0.min.js, line 2)
Deep Copy
Ao contrário da shallow copy, uma deep copy cria uma nova instância do objeto original e também cria novas instâncias de todos os objetos referenciados. Isso significa que todas as propriedades e membros do objeto original são copiados, não apenas as referências. Como resultado, as alterações feitas em um objeto não afetam o outro.
Uma das formas de realizar uma deep copy é através do uso do structuredClone
, que é razoavelmente recente, mas suportado pelos navegadores atuais e também a partir da versão 17 do Node.js. Veja o exemplo abaixo:
const obj = {
nome: "consolelog.com.br",
tags: [1, 2, 3],
objs: {
item1: "valor1",
item2: "valor2",
},
data: new Date("2023-12-31T23:59:59.000-0300"),
};
const clone = structuredClone(obj);
console.log(
typeof obj, // object
typeof obj.nome, // string
typeof obj.tags, // object
typeof obj.objs // object
);
// Alterando o valor do nome, que é do
// tipo primitivo:
clone.nome = "https://consolelog.com.br";
console.log(
obj === clone, // false
obj.nome === clone.nome, // false
obj.tags === clone.tags, // false
obj.objs === clone.objs, // false
obj.data === clone.data // false
);
// Problema:
console.log(
obj.data, // Sun Dec 31 2023 23:59:59 GMT-0300 (Brasilia Standard Time)
obj.data instanceof Date, // true
clone.data, // Sun Dec 31 2023 23:59:59 GMT-0300 (Brasilia Standard Time)
clone.data instanceof Date // true
);
Se o objeto original possuir alguma função que não é serializável, você tomará o seguinte erro: DataCloneError
DataCloneError
DOMException
Thrown if any part of the input value is not serializable.
https://developer.mozilla.org/en-US/docs/Web/API/structuredClone#exceptions
Um outro exemplo envolvendo datas:
const obj = {
data: new Date("2023-08-01T12:00:00.000-0300")
};
// Trecho comentado e utilizado no
// exemplo shallow copy
// const clone = Object.assign({}, obj);
const clone = structuredClone(obj);
// Adiciona 10 minutos
const dezMinutos = 10 * 60 * 1e3;
clone.data.setTime(clone.data.getTime() + dezMinutos);
console.log(clone.data);
console.log(obj.data);
Embora tenhamos o recurso structuredClone
nativo, pessoalmente, desencorajo o uso de bibliotecas ou mesmo da combinação JSON.stringify + JSON.parse
. No entanto, para fins de estudo, vamos examinar o método JSON.parse(JSON.stringify(obj))
:
const obj = {
nome: "consolelog.com.br",
tags: [1, 2, 3],
objs: {
item1: "valor1",
item2: "valor2",
},
data: new Date("2023-12-31T23:59:59.000-0300"),
funcao: () => console.log('ola'),
};
const clone = JSON.parse(JSON.stringify(obj));
console.log(
typeof obj, // object
typeof obj.nome, // string
typeof obj.tags, // object
typeof obj.objs // object
);
// Alterando o valor do nome, que é do
// tipo primitivo:
clone.nome = "https://consolelog.com.br";
console.log(
obj === clone, // false
obj.nome === clone.nome, // false
obj.tags === clone.tags, // false
obj.objs === clone.objs, // false
obj.data === clone.data, // false
obj.funcao === clone.funcao // false
);
// Problema:
console.log(
obj.data, // Sun Dec 31 2023 23:59:59 GMT-0300 (Brasilia Standard Time)
obj.data instanceof Date, // true
clone.data, // 2024-01-01T02:59:59.000Z
clone.data instanceof Date, // false
typeof obj.funcao, // function
typeof clone.funcao // undefined
);
Podemos destacar alguns pontos do código acima:
- tivemos problemas na cópia do objeto
Date
. Quando executamos oJSON.stringify
, o objetoDate
é convertido para o formato ISO8601, ou seja, umastring
. Então quando efetuarmos oparse
a data continuará comostring
e não um objetoDate
. - Não funcionará com
Map
eSet
Considerações
Ao clonar objetos em JavaScript, é importante entender a diferença entre deep clone e shallow clone. O deep clone cria uma cópia completa e independente do objeto, incluindo todos os seus níveis de aninhamento. Por outro lado, o shallow clone cria uma cópia superficial, onde as propriedades de níveis mais profundos são compartilhadas entre o objeto original e o clone.
Existe uma diferença entre performance destes dois métodos. Fiz uma pequena simulação apenas para dar uma ideia. Veja abaixo:
const obj = {
"product": "Live JSON generator",
"version": 3.1,
"releaseDate": "2014-06-25T00:00:00.000Z",
"demo": true,
"person": {
"id": 12345,
"name": "John Doe",
"phones": {
"home": "800-123-4567",
"mobile": "877-123-1234"
},
"email": [
"jd@example.com",
"jd@example.org"
],
"dateOfBirth": "1980-01-02T00:00:00.000Z",
"registered": true,
"emergencyContacts": [{
"name": "Jane Doe",
"phone": "888-555-1212",
"relationship": "spouse"
},
{
"name": "Justin Doe",
"phone": "877-123-1212",
"relationship": "parent"
}
]
}
}
console.time('shallow copy');
const shallowClone = {
...obj
};
console.timeEnd('shallow copy');
console.time('deep copy');
const deepCopy = structuredClone(obj);
console.timeEnd('deep copy');
// Output:
// shallow copy: 0.004ms
// deep copy: 0.027ms
É possível observar que existe uma diferença significativa na performance. Então ao lidar com objetos grandes ou complexos, também é essencial considerar o impacto na performance.
Em resumo, a escolha entre deep e shallow copy depende das necessidades de cada caso.
Links:
- https://medium.com/version-1/cloning-an-object-in-javascript-shallow-copy-vs-deep-copy-fa8acd6681e9#:~:text=There are two ways to,of the object are copied
- https://www.javascripttutorial.net/object/3-ways-to-copy-objects-in-javascript/
- https://developer.mozilla.org/en-US/docs/Web/API/structuredClone
- https://github.com/nodejs/node/issues/34355