Ravakahn Gladior
Ravakahn Gladior Ravakahn foi um gladiador que, na Roma Antiga, lutava com outros gladiadores ou animais, às vezes, até a morte, para o entretenimento do público romano. há 4 anos

Cópia profunda de objetos e arrays no Javascript

Cópia profunda de objetos e arrays no Javascript

Sumário

O que é uma cópia superficial?

Fazer uma cópia superficial de um objeto (ou array) significa criar novas referências aos valores primitivos dentro do objeto (para os entendedores de orientação a objetos, clonar).

Isso significa que alterações no objeto original não afetarão o novo objeto, o que aconteceria se apenas a referência ao objeto fosse copiado (como ocorre com o operador de atribuição =).

Uma cópia superficial refere-se ao fato de que apenas um nível é copiado e que funcionará bem para um objeto contendo apenas valores primitivos.

Para objetos/arrays que contêm outros objetos/arrays, o processo de cópia desses objetos requer uma cópia profunda. Caso contrário, as alterações feitas nas referências aninhadas alterarão os dados aninhados no objeto ou array original.

Neste artigo, descrevo 4 métodos para fazer uma cópia superficial e depois 5 métodos para fazer uma cópia profunda em JavaScript.

1. Cópia superficial com o operador de espalhamento (...)

O operador de espalhamento (...), ou originalmente spread operator, é uma forma conveniente para fazer uma cópia superficial.

const array = ['😉','😊','😇']

// Mudancas em array afetarao copyWithEquals
const copyWithEquals = array
// true (O operador = nao faz uma copia)
console.log(copyWithEquals === array)

// Mudancas em array nao afetarao copyWithSpread
const copyWithSpread = [...array]
// false (O operador ... faz uma copia superficial)
console.log(copyWithSpread === array)

array[0] = '😡' // Whoops, foi sem querer

console.log(...array) // 😡 😊 😇
console.log(...copyWithEquals) // 😡 😊 😇
console.log(...copyWithSpread) // 😉 😊 😇

Como mostrado acima, o operador de espalhamento é útil para criar novas instancias de arrays que não serão afetadas por mudanças a referências antigas.

2. Cópia superficial com .slice()

Especificamente para arrays, usando o método Array.slice() funciona igual ao operador de espalhamento, criando uma cópia superficial de um nível:

const array = ['😉','😊','😇']

// Mudancas em array afetarao copyWithEquals
const copyWithEquals = array
// true (O operador = nao faz uma copia)
console.log(copyWithEquals === array)

// Mudancas em array nao afetarao copyWithSpread
const copyWithSpread = array.slice()
 // false (Usando .slice() voce faz uma copia superficial de array)
console.log(copyWithSpread === array)

array[0] = '😡' // Whoops, foi sem querer

console.log(...array) // 😡 😊 😇
console.log(...copyWithEquals) // 😡 😊 😇
console.log(...copyWithSpread) // 😉 😊 😇

3. Cópia superficial com .assign()

O mesmo tipo de cópia superficial pode ser criado utilizando o Object.assign(), que pode ser usado com qualquer objeto ou array:

const array = ['😉','😊','😇']

// Mudancas em array afetarao copyWithEquals
const copyWithEquals = array
// Mudancas em array nao afetarao copyWithAssign
const copyWithAssign = []
Object.assign(copyWithAssign, array) // Object.assign(target, source)

array[0] = '😡' // Whoops, foi sem querer

console.log(...array) // 😡 😊 😇
console.log(...copyWithEquals) // 😡 😊 😇
console.log(...copyWithAssign) // 😉 😊 😇

4. Cópia superficial de arrays com Array.from()

Outro método para copiar um array no Javascript é usando Array.from(), que irá fazer fazer uma cópia superficial, conforme o exemplo:

const emojiArray = ["😎", "😊", "😇"]
console.log(...emojiArray) // 😎 😊 😇

// O operador de atribuicao = ira copiar a referencia do array original
const emojiArrayNotCopied = emojiArray
// true - as duas variaveis fazem referencia ao array original
console.log(emojiArrayNotCopied === emojiArray)

// Faz uma copia superficial usando Array.from
const emojiArrayShallowCopy = Array.from(emojiArray)
// false -- as variaveis fazem referencia a diferentes arrays
console.log(emojiArrayShallowCopy === emojiArray)

// Mudar um valor no array original
emojiArray[0] = "😠"

// O operador de atribuicao nao faz uma copia do array, portanto as mudancas no array original afetarao o array nao copiado 
console.log(...emojiArray) // 😠 😊 😇
console.log(...emojiArrayNotCopied) // 😠 😊 😇
console.log(...emojiArrayShallowCopy) // 😎 😊 😇

// Como outros metodos de cópia superficial, o Array.from() ira criar uma cópia superficial para arrays.
// Mas para arrays aninhados em profundidade que contém outros valores não primitivos (como objetos e arrays), você precisa fazer uma cópia profunda, o que Array.from() não consegue fazer.

Se um objeto ou array contém outros objetos ou arrays, as cópias superficiais irão funcionar inesperadamente, porque objetos aninhados não serão, de fato, clonados (OO).

Para objetos aninhados em profundidade, uma cópia profunda será necessário. Vamos às explicações.

Cuidado com os valores aninhados

Quando objetos estão aninhados em profundidade, o operador de espalhamento copia apenas o primeiro nível para uma nova referência, mas os valores mais profundos continuam na mesma referência.

const nestedArray = [['😉'],['😊'],['😇']]
const nestedCopyWithSpread = [...nestedArray]

nestedArray[0][0] = '😡' // Whoops, foi sem querer

console.log(...nestedArray) // ["😡"] ["😊"] ["😇"]
console.log(...nestedCopyWithSpread) // ["😡"] ["😊"] ["😇"]

// Este é um hack para fazer uma cópia profunda que não é recomendado pois pode falhar
const nestedCopyWithHack = JSON.parse(JSON.stringify(nestedArray)) // cópia profunda
// Somente este array copiado será modificado
nestedCopyWithHack[0][0] = '😉'

console.log(...nestedArray) // ["😡"] ["😊"] ["😇"]
console.log(...nestedCopyWithSpread) // ["😡"] ["😊"] ["😇"]
console.log(...nestedCopyWithHack) // ["😉"] ["😊"] ["😇"]

// Uma solução melhor é usar uma biblioteca como lodash ou métodos como clone() do Ramda

Para resolver este problema é necessário criar uma cópia profunda, o oposto de cópia superficial. Cópias profundas podem ser feitas usando lodash, rfdc ou usando o método R.clone() da biblioteca funcional Ramda. Essas sugestões são exploradas a seguir.

O que é uma cópia profunda?

Para objetos que contêm outros objetos, a cópia desses objetos requer uma cópia profunda. Caso contrário, as alterações feitas nas referências aninhadas alterarão os dados aninhados no objeto ou matriz original.

Isto é comparado com uma cópia superficial, que funciona muito bem para objetos que contém apenas valores primitivos, mas irá falhar com qualquer objeto ou array que tenha referências aninhadas a outros objetos.

Compreender a diferença entre os operadores == e === pode ajuda-lo a ver a diferença entre a cópia superficial e profunda, pois o operador de igualdade estrita (===) mostra que as referências aninhadas são as mesmas:

const nestedArray = [["😉"], ["😊"], ["😇"]]
const nestedCopyWithSpread = [...nestedArray]

// true - copia superficial (mesma referencia)
console.log(nestedArray[0] === nestedCopyWithSpread[0])

const nestedCopyWithHack = JSON.parse(JSON.stringify(nestedArray))

// false - copia profunda (referencias diferentes)
console.log(nestedArray[0] === nestedCopyWithHack[0])

Agora irei falar sobre 5 métodos de fazer uma cópia profunda (ou clone profundo): lodash, Ramda, uma função personalizada, JSON.parse() / JSON.stringify(), e rfdc.

1. Cópia profunda com lodash

Site oficial: https://lodash.com/

A biblioteca lodash é a maneira mais comum de os desenvolvedores JavaScript fazerem uma cópia profunda. É surpreendentemente fácil de usar:

// Importar a lib lodash por completo
import _ from "lodash"
// Alternativa: importar somente os métodos de clone
// import { clone, cloneDeep } from "lodash"

// Este array está aninhado em 1 nível, embora o comportamento seja o mesmo com qualquer grau de aninhamento
const nestedArray = [["😉"], ["😊"], ["😇"]]

const notACopyWithEquals = nestedArray
// true - não é uma cópia (mesma referência)
console.log(nestedArray[0] === notACopyWithEquals[0])

const shallowCopyWithSpread = [...nestedArray]
// true - cópia superficial (mesma referência)
console.log(nestedArray[0] === shallowCopyWithSpread[0])

const shallowCopyWithLodashClone = _.clone(nestedArray)
// true - cópia superficial (mesma referência)
console.log(nestedArray[0] === shallowCopyWithLodashClone[0])

const deepCopyWithLodashCloneDeep = _.cloneDeep(nestedArray)
// false - cópia profunda (referências diferentes)
console.log(nestedArray[0] === deepCopyWithLodashCloneDeep[0])

// Tente alterar a referência do primeiro elemento, não irá funcionar para nenhuma cópia
nestedArray[0] = "🧐"

// Tente alterar a referência aninhado do terceiro elemento
nestedArray[2][0] = "😈"

console.log(...nestedArray) // 🧐 ["😊"] ["😈"]
console.log(...notACopyWithEquals) // 🧐 ["😊"] ["😈"]
console.log(...shallowCopyWithSpread) // ["😉"] ["😊"] ["😈"]
console.log(...shallowCopyWithLodashClone) // ["😉"] ["😊"] ["😈"]
console.log(...deepCopyWithLodashCloneDeep) // ["😉"] ["😊"] ["😇"]

2. Cópia profunda com Ramda

Site oficial: https://ramdajs.com/
Método clone: https://ramdajs.com/docs/#clone

A biblioteca de programação funcional Ramda inclui o método R.clone() que faz uma cópia profunda de um objeto ou array.

// Importar a biblioteca ramda
import R from "ramda"
// alternativamente, importar somente os métodos de clone
// import { clone } from "ramda"

// Este array está aninhado em 1 nível, embora o comportamento seja o mesmo com qualquer grau de aninhamento
const nestedArray = [["😉"], ["😊"], ["😇"]]

const notACopyWithEquals = nestedArray
// true - não é uma cópia (mesma referência)
console.log(nestedArray[0] === notACopyWithEquals[0])

const shallowCopyWithSpread = [...nestedArray]
// true - cópia superficial (mesma referência)
console.log(nestedArray[0] === shallowCopyWithSpread[0])

const deepCopyWithRamdaClone = R.clone(nestedArray)
// falso - cópia profunda (referências diferentes)
console.log(nestedArray[0] === deepCopyWithRamdaClone[0])

// Tente alterar a referência do primeiro elemento, nao ira funcionar para nenhuma cópia
nestedArray[0] = "🧐"

// Tente alterar a referência aninhado do terceiro elemento
nestedArray[2][0] = "😈"

console.log(...nestedArray) // 🧐 ["😊"] ["😈"]
console.log(...notACopyWithEquals) // 🧐 ["😊"] ["😈"]
console.log(...shallowCopyWithSpread) // ["😉"] ["😊"] ["😈"]
console.log(...deepCopyWithRamdaClone) // ["😉"] ["😊"] ["😇"]

Perceba que o método R.clone() da biblioteca Ramda é equivalente a _.cloneDeep() do lodash, e que o Ramda não possui um método para cópias superficiais.

3. Cópia profunda com uma função personalizada

É muito fácil escrever uma função recursiva que irá fazer uma cópia profunda de objetos ou arrays aninhados. Veja o exemplo:

const deepCopyFunction = (inObject) => {
  let outObject, value, key

  if (typeof inObject !== "object" || inObject === null) {
    // retorna o valor de entrada se inObject não é objeto
    return inObject
  }

  // Cria um objeto ou array para armazenar os valores
  outObject = Array.isArray(inObject) ? [] : {}

  for (key in inObject) {
    value = inObject[key]

    // Cópia recursiva (profunda) para objetos aninhados
    outObject[key] = deepCopyFunction(value)
  }

  return outObject
}

let originalArray = [37, 3700, { hello: "world" }]
// 37 3700 Object { hello: "world" }
console.log("Original array:", ...originalArray)

let shallowCopiedArray = originalArray.slice()
let deepCopiedArray = deepCopyFunction(originalArray)

// vai afetar somente o array original
originalArray[1] = 0
console.log(`originalArray[1] = 0 // vai afetar somente o array original`)
// vai afetar o array original e a cópia superficial
originalArray[2].hello = "moon"
console.log(`originalArray[2].hello = "moon" // vai afetar o array original e a cópia superficial`)

// 37 0 Object { hello: "moon" }
console.log("Original array:", ...originalArray)
// 37 3700 Object { hello: "moon" }
console.log("Shallow copy:", ...shallowCopiedArray)
// 37 3700 Object { hello: "world" }
console.log("Deep copy:", ...deepCopiedArray)

Cuidado! Você precisa verificar o valor null porque typeof null == "object" retorna true. (How to check for null in JavaScript)

console.log(typeof null) // "object"

4. Cópia profunda com JSON.parse/stringify

Se os seus dados se encaixam nas especificações (veja abaixo), então o JSON.parse seguido de um JSON.stringify irá fazer uma cópia produnda no seu objeto.

If you do not use Dates, functions, undefined, Infinity, [NaN], RegExps, Maps, Sets, Blobs, FileLists, ImageDatas, sparse Arrays, Typed Arrays or other complex types within your object, a very simple one liner to deep clone an object is: JSON.parse(JSON.stringify(object))

Dan Dascalescu em sua resposta no StackOverflow

Simplificando (e traduzindo), se você não trabalha com Dates, functions, undefined, Infinity, [NaN], RegExp, Map, Set, Blob, FileList, ImageData, sparse Array, Typed Array ou outros tipos complexos no seu objeto, você pode fazer uma cópia profunda só com uma linha de código.

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

Para demonstrar alguns motivos pelo qual este método não é recomendado, veja um exemplo:

// Somente alguns tipos de valores podem ser copiados com JSON.parse/stringify
const sampleObject = {
  string: 'string',
  number: 123,
  boolean: false,
  null: null,
  notANumber: NaN, // valores NaN serão forcados para null
  date: new Date('1999-12-31T23:59:59'),  // Date será convertido para string
  undefined: undefined,  // valores undefined serão completamente perdidos, incluindo a chave que contem o valor
  infinity: Infinity,  // valores Infinity serão forcados para null
  regExp: /.*/, // valor RegExp serão forcados para um objeto vazio {}
}

// Object { string: "string", number: 123, boolean: false, null: null, notANumber: NaN, date: Date Fri Dec 31 1999 23:59:59 GMT-0500 (Eastern Standard Time), undefined: undefined, infinity: Infinity, regExp: /.*/ }
console.log(sampleObject)
console.log(typeof sampleObject.date) // object

const faultyClone = JSON.parse(JSON.stringify(sampleObject))

// Object { string: "string", number: 123, boolean: false, null: null, notANumber: null, date: "2000-01-01T04:59:59.000Z", infinity: null, regExp: {} }
console.log(faultyClone)

// O objeto date vai ser stringfado
console.log(typeof faultyClone.date) // string

Uma function personalizada ou as bibliotecas mencionadas podem fazer uma cópia profunda sem a necessidade de se preocupar com o tipo de conteúdo, embora precisamos tomar um cuidado especial com as referências circulares.

A seguir, discuto a biblioteca rfdc, que pode lidar com referências circulares e ser tão rápida quanto uma função de cópia profunda personalizada.

5. Quer uma cópia profunda extremamente rápida? Pense em rfdc

Site oficial: https://www.npmjs.com/package/rfdc
Documentação: https://github.com/davidmarkclements/rfdc

Para obter o melhor desempenho, a biblioteca rfdc (Really Fast Deep Clone) copiará em profundidade cerca de 400% mais rápido que o _.cloneDeep da lodash:

Veja a descrição removida da própria documentação do rfdc

O rdfc clona todos os tipos JSON: Object, Array, Number, String e null.

Com suporte adicional para: Date (copiado), Undefined (copiado), Function (referenciado), AsyncFunction (referenciado), GeneratorFunction (referenciado) e argumentos (copiado para um objeto simples)

Todos os outros tipos têm valores de saída que correspondem à saída de JSON.parse (JSON.stringify (o))

O uso do rfdc é muito direto:

// Obtem a funcao de copia profunda
const clone = require('rfdc')()
// {a: 37, b: {c: 3700}}
clone({a: 37, b: {c: 3700}})

A biblioteca rfdc suporta todos os tipos de dados e também suporta referências circulares com uma flag opcional que diminui o desempenho em cerca de 25%. As referências circulares irão quebrar os algoritmos de cópia profunda discutidos até agora.

Essa biblioteca seria útil se você estiver lidando com um objeto grande e complexo, como um carregado em arquivos JSON com tamanho de 3 MB a 15 MB.

Aqui estão os benchmarks, mostrando que o rfdc é cerca de 400% mais rápido ao lidar com objetos grandes:

benchLodashCloneDeep100: 1461.134ms
benchRfdc
100: 323.899ms
benchRfdcCircles*100: 384.561ms

Fonte: documentação rfdc

A lib rfdc (Really Fast Deep Clone) está disponível no GitHub e npm:

Performance dos algoritmos de cópia no Javascript

Dos vários algoritmos de cópia, as cópias superficiais são as mais rápidas, seguidas de cópias profundas usando uma função personalizada ou rfdc.

Veja o comparativo de formas de copiar objetos/arrays que encontrei em uma resposta no StackOverflow - Tim Montague in his StackOverflow answer

Deep copy by performance: Ranked from best to worst
Reassignment "=" (string arrays, number arrays - only)
Slice (string arrays, number arrays - only)
Concatenation (string arrays, number arrays - only)
Custom function: for-loop or recursive copy
jQuery's $.extend
JSON.parse (string arrays, number arrays, object arrays - only)
Underscore.js's .clone (string arrays, number arrays - only)
Lo-Dash's
.cloneDeep

Conclusão

Na verdade, é muito fácil evitar as cópias em profundidade - é só evitar o aninhamento de objetos/arrays dentro um do outro.

Porque nesse caso - onde não há aninhamento e os objetos contêm apenas valores primitivos - fazer uma cópia superficial com o operador de espalhamento (...), Array.slice() ou Object.assign() funcionam muito bem.

Mas, no mundo real, onde os objetos/arrays não possuem apenas valores primitivos, é necessário usar algoritmos de cópia profunda. Pelo que vemos, o rfdc é o mais rápido para cópias profundas.