Artigo original: https://www.freecodecamp.org/news/copying-stuff-in-javascript-how-to-differentiate-between-deep-and-shallow-copies-b6d8c1ef09cd/

Escrito por Lukas Gisder-Dubé

O novo é sempre melhor!

Você certamente já lidou com cópias em JavaScript antes, mesmo que não soubesse. Talvez você também já tenha ouvido falar do paradigma na programação funcional, onde você não deve modificar nenhum dado existente. Para fazer isso, você precisa saber como copiar valores com segurança em JavaScript. Hoje, veremos como fazer isso evitando as armadilhas!

Em primeiro lugar, o que é uma cópia?

Uma cópia apenas se parece com o original, mas não é. Quando você altera a cópia, você espera que o original permaneça a mesmo, enquanto a cópia muda.

Na programação, armazenamos valores em variáveis. Fazer uma cópia significa que você inicia novas variáveis com os mesmos valores. No entanto, há uma grande armadilha potencial a ser considerada: a diferença entre cópia profunda (do inglês, deep copy) e a cópia superficial (do inglês, shallow copy). Uma cópia profunda significa que todos os valores da nova variável são copiados e desconectados da variável original. Uma cópia superficial significa que certos (sub)valores ainda estão conectados a ela.

Para realmente entender a cópia, você precisa saber como o JavaScript armazena valores.

Tipos de dados primitivos

Os tipos de dados primitivos incluem:

  • Numbers (ou números) — ex.: 1
  • Strings — ex.: 'Olá'
  • Booleanos — ex.: true
  • undefined (ou indefinido)
  • null (ou nulo)

Quando você cria esses valores, eles são fortemente acoplados à variável a qual são atribuídos. Eles só existem uma vez. Isso significa que você realmente não precisa se preocupar em copiar tipos de dados primitivos em JavaScript. Quando você faz uma cópia, será uma cópia real. Vejamos um exemplo:

const a = 5
let b = a // this is the copy
b = 6
console.log(b) // 6
console.log(a) // 5

Ao executar b = a, você faz uma cópia. Agora, quando você reatribui um novo valor a b, o valor de b muda, mas não o valor de a.

Tipos de dados compostos — objetos e arrays

Tecnicamente, os arrays também são objetos. Então, eles se comportam da mesma maneira. Examinarei os dois em detalhes mais tarde.

Aqui fica mais interessante. Na verdade, esses valores são armazenados apenas uma vez quando instanciados. Atribuir uma variável apenas cria um ponteiro (referência) para esse valor.

Agora, se fizermos uma cópia b = a e se alterarmos algum valor aninhado em b, ele também alterará, de fato, o valor aninhado de a, já que a e b apontam para a mesma coisa. Exemplo:

const a = {
en: 'Hello',
  de: 'Hallo',
  es: 'Hola',
  pt: 'Olá'
}
let b = a
b.pt = 'Oi'
console.log(b.pt) // Oi
console.log(a.pt) // Oi

No exemplo acima, nós realmente fizemos uma cópia superficial. Isso muitas vezes é problemático, já que esperamos que a variável antiga tenha os valores originais, não os valores alterados. Quando a acessamos, às vezes, recebemos um erro. Pode acontecer que você passe algum tempo tentando fazer o debugging antes de encontrar o erro, já que muitos desenvolvedores realmente não entendem o conceito e não esperam que esse seja o erro.

1_niSsr1dxva2RWXrvHT8hxg
Foto criada por Thomas Millot e extraída do Unsplash

Vamos dar uma olhada em como podemos fazer cópias de objetos e arrays.

Objetos

Existem várias maneiras de se fazer cópias de objetos, especialmente com a nova especificação do JavaScript em expansão e aprimoramento.

Operador spread

Introduzido com a ES2015, este operador é simplesmente ótimo, porque ele é curto e simples. Ele "espalha" (em inglês, spread) todos os valores em um novo objeto. Você pode usá-lo da seguinte maneira:

const a = {
  en: 'Bye',
  de: 'Tschüss'
}
let b = {... a}
b.de = 'Ciao'
console.log(b.de) // Ciao
console.log(a.de) // Tschüss

Você também pode usá-lo para mesclar dois objetos, por exemplo: const c = {...a, ...b} .

Object.assign

Este método era usado principalmente antes da criação do operador spread. Basicamente, ele faz a mesma coisa. No entanto, você precisa ter cuidado, pois o primeiro argumento no método Object.assign() realmente é modificado e retornado. Portanto, certifique-se de passar o objeto a ser copiado pelo menos como o segundo argumento. Normalmente, você apenas passaria um objeto vazio como o primeiro argumento para impedir a modificação de quaisquer dados existentes.

const a = {
en: 'Bye',
  de: 'Tschüss'
}
let b = Object.assign({}, a)
b.de = 'Ciao'
console.log(b.de) // Ciao
console.log(a.de) // Tschüss

Armadilha: objetos aninhados

Armadilha: objetos aninhados

Como mencionado anteriormente, há uma grande ressalva ao lidar com a cópia de objetos, que se aplica aos dois métodos listados acima. Quando você tem um objeto (ou array) aninhado e o copia, os objetos aninhados dentro desse objeto não serão copiados, pois são apenas ponteiros/referências. Portanto, se você alterar o objeto aninhado, alterará em ambas as instâncias, o que significa que você acabaria fazendo uma cópia superficial novamente.

Exemplo: // MAU EXEMPLO, NO CASO

const a = {
  alimentos: {
    jantar: 'Massa'
  }
}
deixe b = {... a}
b.alimentos.jantar = 'Sopa' // mudanças para ambos os objetos
console.log(b.alimentos.jantar) // Sopa
console.log(a.alimentos.jantar) // Sopa

Para fazer uma cópia profunda de objetos aninhados, você teria que considerar isso. Uma maneira de evitar isso é copiar manualmente todos os objetos aninhados:

const a = {
  alimentos: {
    jantar: 'Massa'
  }
}
let b = {alimentos: {... a.alimentos}}
b.alimentos.jantar = 'Sopa'
console.log(b.alimentos.jantar) // Sopa
console.log(a.alimentos.jantar) // Massa

Caso você esteja se perguntando o que fazer quando o objeto tem mais chaves do que apenas alimentos, você pode usar todo o potencial do operador spread. Ao passar mais propriedades após o spread, ..., eles substituem os valores originais, por exemplo, const b = {...a, alimentos: {...a.alimentos}}.

Como fazer cópias profundas sem pensar

Se você não souber a profundidade das estruturas aninhadas, o que pode fazer? Pode ser muito tedioso passar manualmente por objetos grandes e copiar todos os objetos aninhados à mão. Há uma maneira de copiar tudo sem pensar. Você simplesmente transforma seu objeto com stringify e usa parse logo em seguida:

const a = {
  alimentos: {
    jantar: 'Massa'
  }
}
let b = JSON.parse(JSON.stringify(a))
b.alimentos.jantar = 'Sopa'
console.log(b.alimentos.jantar) // Sopa
console.log(b.alimentos.jantar) // Massa

Aqui, você deve considerar que não poderá copiar instâncias de classe personalizadas. Portanto, só poderá usá-las quando copiar objetos com valores nativos do JavaScript dentro deles.

1_hepu5hNOaAqnhE60z-oicA
Foto criada por Robert Zunikoff, extraída do Unsplash

Arrays

Copiar arrays é tão comum quanto copiar objetos. Grande parte da lógica por trás disso é semelhante, já que os arrays também são apenas objetos internamente.

Operador spread

Assim como acontece com os objetos, você pode usar o operador spread para copiar um array:

const a = [1,2,3]
let b = [... a]
b[1] = 4
console.log(b[1]) // 4
console.log(a[1]) // 2

Funções dos arrays — map, filter e reduce

Estes métodos retornarão um novo array com todos (ou alguns) valores do array original. Ao fazer isso, você também pode modificar os valores, o que é muito útil:

const a = [1,2,3]
let b = a.map(el => el)
b[1] = 4
console.log(b[1]) // 4
console.log(a[1]) // 2

Como opção, você pode mudar o elemento desejado enquanto o copia:

const a = [1,2,3]
const b = a.map((el, indice) = indice > === 1 ? 4 : el)
console.log(b[1]) // 4
console.log(a[1]) // 2

Array.slice

Esse método é normalmente usado para retornar um subconjunto dos elementos, começando em um índice específico e, como opção, terminando em um índice específico do array original. Ao usar array.slice() ou array.slice(0), você acabará com uma cópia do array original.

const a = [1,2,3]
let b = a.slice(0)
b[1] = 4
console.log(b[1]) // 4
console.log(a[1]) // 2

Arrays aninhados

Semelhante aos objetos, o uso dos métodos acima para copiar um array com outro array ou objeto aninhado gerará uma cópia superficial. Para evitar isso, use também JSON.parse(JSON.stringify(nomeDoArray)).

EXTRA: como copiar instâncias de classes personalizadas

Quando você já é um profissional em JavaScript e lida com suas funções ou classes de construtor personalizadas, talvez queira copiar instâncias delas também.

Como mencionado anteriormente, você não pode simplesmente usar stringify + parse nesses casos, pois perderá seus métodos de classe. Em vez disso, você desejará adicionar um método de copiar personalizado para criar uma instância com todos os valores antigos. Vejamos como isso funciona:

classe Contador{
  constructor() {
     this.contagem = 5
  }
  copiar() {
    const copia = new Contador()
    copia.contagem = this.contagem
    return copia
  }
}
const contadorOriginal = novo Contador()
const contadorCopiado = contadorOriginal.copiar()
console.log(contadorOriginal.contagem) // 5
console.log(contadorCopiado.contagem) // 5
contadorCopiado.contagem = 7
console.log(contadorOriginal.contagem) // 5
console.log(contadorCopiado.contagem) // 7

Para lidar com objetos e arrays que são referenciados dentro de sua instância, você teria que aplicar suas habilidades recém-aprendidas sobre cópia profunda! Vou apenas adicionar uma solução final para o método de cópia do construtor personalizado para torná-lo mais dinâmico:

class Contador {
  constructor() {
    this.contagem = 5

    this.adicionar = function() {
      this.contagem++
    }
  }

  copiar() {
    const copia = new Contador()

    Object.keys(this).forEach(chave => {
      const valor = this[chave]

      switch(typeof valor) {
        case 'function':
          break
        case 'object':
          copy[chave] = JSON.stringify(JSON.parse(valor))
          break
        default:
          copy[chave] = valor
          break
      }
    })
    
    return copia
  }
}

Com esse método de cópia, você pode colocar quantos valores quiser em seu construtor, sem ter que copiar tudo manualmente!

Sobre o autor: Lukas Gisder-Dubé é cofundador e foi o líder de uma startup como CTO por 1 ano e meio, gerindo a equipe de tecnologia e arquitetura. Depois de deixar a startup, ele ensinou programação como instrutor principal na Ironhack e agora está criando uma agência de startups e consultoria em Berlim. Confira o site do autor, dube.io, para saber mais.

1*p-l0Cee1IHvX0RQkVTOceQ