DEV Community

Guilherme Rodrigues
Guilherme Rodrigues

Posted on • Edited on

Clonando objetos de forma segura no Javascript

Acredito que todos, assim como eu, já precisaram fazer uma cópia de um objeto no Javascript e, felizmente, temos diversas formas de copiar um objeto. Dentre as mais conhecidas temos o Object.assing e o Spread Operator

Object.assign

A primeira forma, e também a mais conhecida, é através do Object.assign que, basicamente, consiste em 2 argumentos. O primeiro é o objeto de destino, ou seja, o novo objeto que irá receber os valores do objeto original, o segundo é o objeto que você deseja copiar (que vamos chamar de objetoOrigem)..

var objetoOrigem = { name: 'Chandler Bing', animal: 'dog' }

var objetoClonado = Object.assign({}, objetoOrigem)
Enter fullscreen mode Exit fullscreen mode

O método .assign retorna um novo objeto, sendo assim podemos alterar as propriedades do novo objeto sem alterar o objetoOrigem.

objetClonado.name = 'Joey Tribbiani'

console.log(objetoOrigem)
// { name: 'Chandler Bing', animal: 'dog' }

console.log(objetoClonado)
//  { name: 'Joey Tribbiani', animal: 'dog' }
Enter fullscreen mode Exit fullscreen mode

Spread Operator

A segunda forma é conhecida como Spread Operator, que consiste em expandir as propriedades dentro do objeto (como string, números e array) para 1 ou n propriedades, em outras palavras, consiste em expandir um objeto maior em várias propriedades daquele objeto, no exemplo fica mais claro.

var array = [1,2,3]

var fn = function(a,b,c) {
  console.log(a,b,c)
  //  1 2 3
}

fn(...array)
Enter fullscreen mode Exit fullscreen mode

Com o Spread Operator, eu posso quebrar o objeto original em n propriedades. Seguindo essa lógica poderiamos recuperar os atributos do objetoOrigem e construir um novo objeto chamado objetoClonado, veja no exemlo a seguir:

var objetoOrigem = { name: 'Chandler Bing' }

var objetoClonado = { ...objetoOrigem }
Enter fullscreen mode Exit fullscreen mode

Beleza, então quando tentamos alterar o nome do novo objeto clonado, o objeto original ainda manteria com os mesmos valores.

objetoOrigem.name = 'Joey Tribbiani'

console.log(objetoOrigem)
//  { name: 'Chandler Bing' }

console.log(objetoClonado)
//  { name: 'Joey Tribbiani' }
Enter fullscreen mode Exit fullscreen mode

No fim das contas o Spread Operator acaba se tornando um substituto para o Object.assing

Shallow clone

Tanto Objetct.assign quanto Spread, fazem um clone que chamamos de Shallow clone. Shallow clone copia apenas valores enumerados como String, Number e Array. Quando clonamos um objeto que possui uma chave cujo valor é outro objeto, o que o Shallow clone faz é copiar a referencia de memória para o novo objeto clonado, sendo assim, os dois objetos compartilham a mesma referência.

Deep clone

Deep clone se baseia em criar um novo objeto a partir do objeto original, criando uma nova referência de memória para os nested objects, caso exista. Existem algumas formas de fazer.

Shallow Clone vs Deep Clone

As duas formas funcionam muito bem, mas só quando estamos lidando com objetos simples, ou seja, objetos compostos de valores primitivos, quando começamos a lidar com objetos mais complexos onde temos nested objects ou funcões, qualquer uma das abordagens listada a cima se tornam inviáveis, o por que ? bem podemos ver em um exemplo real, vamos considerar o seguinte objeto.

var objetoOrigem = {
  name: 'Chandler Bing',
  age: 25,
  job: {
    name: 'Unknown'
  }
}
Enter fullscreen mode Exit fullscreen mode

Ao clonarmos o objeto e modificar o nome do objeto clonado obteremos o seguinte resultado.

var objetoClonado = { ...objetoOrigem }

objetoClonado.name = 'Joey Tribbianiy'

console.log(objetoOrigem)
//  { name: 'Chandler Bing', age: 25, job: { name: 'Unknown' } }

console.log(objetoClonado)
//  { name: 'Joey Tribbiani', age: 25, job: { name: 'Unknown' }
Enter fullscreen mode Exit fullscreen mode

Modificamos o objetoClonado sem alterar o objetoOrigem. Perfeito!

Agora vamos tentar modificar a propriedade job do objeto clonado

objetoClonado.job.name = 'Actor'
Enter fullscreen mode Exit fullscreen mode

E ao verificar o valor temos a seguinte saida:

console.log(objetoOrigem)
//  { name: 'Chandler', age: 25, job: { name: 'Actor' } }

console.log(objetoClonado)
//  { name: 'Joe', age: 25, job: { name: 'Actor' } }
Enter fullscreen mode Exit fullscreen mode

Ao alterar a propriedade objetoClonado.job alterou tanto para objetoClonado quanto para o objetoOrigem.

Deep Clone utilizando JSON.stringify e JSON.parse

Em alguns lugares você pode ter visto um deep clone usando o a implementaçãoJSON.stringify e JSON.parse. que consiste em transformar o seu objeto origem em JSON, e em seguida utilizando o JSON.parse para criar um novo objeto, como mostra o código a baixo.

var objetoOrigem = {
  name: 'Chandler',
  age: 25,
  job: {
    name: 'Unknown'
  },
  myNameAndJob() {
    return `My name is ${this.name} and I work as ${this.job.name}`
  }
}
Enter fullscreen mode Exit fullscreen mode

Dessa vez criamos um objeto que possui uma função que retorna o name e o job em uma única string, agora vamos clonar o objeto.

var objetoClonado = JSON.parse(JSON.stringify(objetoOrigem))
Enter fullscreen mode Exit fullscreen mode

E ao tentar modificar as propriedades do objeto clonado e executar a função myNameAndJob, gera a seguinte saída.

objetoClonado.name = 'Joe'
objetoClonado.job.name = 'Actor'

console.log(objetoOrigem.myNameAndJob())
//  My name is Chandler and I work as Unknown

console.log(objetoClonado.myNameAndJob())
//  console.log(objetoClonado.myNameAndJob())
//  TypeError: objetoClonado.myNameAndJob is not a function
Enter fullscreen mode Exit fullscreen mode

O Erro foi gerado porque, ao utilizar o JSON.stringify no objeto criado, o resultado foi uma string da estrutura de dados do objeto original, ou seja, não existe funções no novo objeto, foi copiado apenas os atributos e os nested objects.

Isso também se torna um problema quando o seu objeto possui propriedades do tipo Date, por exemplo.

var objetoComDate = {
  name: 'Chandler',
  birthday: new Date('1994-01-01T00:00:00')
}

var objetoClonado = JSON.parse(JSON.stringify(objetoComDate))
Enter fullscreen mode Exit fullscreen mode

Ao exibir os dois objetos, note a diferença

Deep_Clone_JSONParse_JSONStringify

O Objeto objetoComDate possui a propriedade birthday como tipo Date, enquanto o objetoClonado transformou a propriedade Date em uma String contendo o valor da data.

Lodash

A forma mais indicada é usando funcionalidades de bibliotecas maduras, testadas e mantida pela comunidade como o Lodash, Lodash é uma biblioteca em Javascript que contém métodos utilitários para trabalhar com Arrays, Objects, String e Numbers.
Podemos instalar o lodash com o comando npm install lodash --save, o legal do Lodash é que podemos importar somente os métodos que vamos usar, assim não carregamos toda a biblioteca de forma necessária.
No Lodash temos um método que faz um deepClone de um objeto, podemos importar o cloneDeep de duas formas

A primeira forma é importar toda a biblioteca e usar o método desejado, como no exemplo abaixo.

var _ = require('lodash')

var objetoOrigem = {
  name: 'Chandler',
  age: 25,
  job: {
    name: 'Unknown'
  },
  myNameAndJob() {
    return `My name is ${this.name} and I work as ${this.job.name}`
  }
}

const objetoClonado = _.cloneDeep(objetoOrigem, {}, true)
Enter fullscreen mode Exit fullscreen mode

A segunda forma é importando somente o método desejado da biblioteca

var _cloneDeep = require('lodash/cloneDeep')

var objetoOrigem = {
  name: 'Chandler',
  age: 25,
  job: {
    name: 'Unknown'
  },
  myNameAndJob() {
    return `My name is ${this.name} and I work as ${this.job.name}`
  }
}

const objetoClonado = _cloneDeep(objetoOrigem, {}, true)
Enter fullscreen mode Exit fullscreen mode

Para qualquer uma das formas, o resultado final será o mesmo, ja que com o cloneDeep é possível clonar o objeto e seus nested objects de forma que o objeto clonado não possua nenhuma referencia compartilhada com o objetoOrigem, como no código abaixo.

var _cloneDeep = require('lodash/cloneDeep')

var objetoOrigem = {
  name: 'Chandler',
  age: 25,
  job: {
    name: 'Unknown'
  },
  myNameAndJob() {
    return `My name is ${this.name} and I work as ${this.job.name}`
  }
}

const objetoClonado = _cloneDeep(objetoOrigem, {}, true)

objetoClonado.name = 'Joe'
objetoClonado.job.name = 'Actor'

console.log(objetoOrigem.myNameAndJob())
//  My name is Chandler and I work as Unknown

console.log(objetoClonadoComClone.myNameAndJob())
//  My name is Joe and I work as Actor
Enter fullscreen mode Exit fullscreen mode

Referencias

Top comments (6)

Collapse
 
dev_jessi profile image
Jéssica Félix

Bom trabalho,Guilherme! Aguardando os próximos!

Collapse
 
guimap profile image
Guilherme Rodrigues

Valeeeu, agradeço de coração pela força e incentivo hahaha.

Collapse
 
thenriquedb profile image
Thiago Henrique Domingues

Acho incriável mesmo programando com JS a um bom tempo, sempre estou descobrindo algo novo sobre a linguagem!

Collapse
 
afifikani profile image
Afif Fikani

Como se clona instâncias de classes?

lodash transforma em plain object ?

Hoje em dia eu uso um design pattern Builder pra isso. Sabes de melhor alternativa?

Parabéns pelo post!

Collapse
 
guimap profile image
Guilherme Rodrigues

Opa Afif, já agradeço pela força e por ter lido o artigo haha.
Sobre a sua dúvida bom vamos lá, cara na minha opinião, se for apenas para fazer um clone, o ideal seria usar ocloneDeepdo lodash

Se tiver alguma dúvida e eu puder ajudar, é só me chamar haha

Collapse
 
deyvisonrocha profile image
Deyvison Rocha

Ótimo tema para trazer para discussão. Valeu