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

¡Nuevo es siempre mejor!

Seguramente has lidiado con copias en JavaScript antes, inclusive si no lo conocías. Tal vez también has oído del paradigma en programación funcional que no deberías modificar ningún dato existente. Para hacer eso, tienes que saber cómo copiar valores de manera segura en JavaScript. Hoy, veremos cómo hacer esto mientras evitamos los obstáculos.

Primero, ante todo, ¿qué es una copia?

Una copia parece como algo viejo, pero no lo es. Cuando cambias la copia, esperas que lo original permanezca lo mismo, donde sea que la copia cambie.

En programación, almacenamos valores en variables. Hacer una copia significa que inicias una nueva variable con el mismo valor o valores. Sin embargo, hay una gran trampa potencial a considerar: copia profunda vs. copia superficial. Una copia profunda significa que todos los valores de la nueva variable son copiados y desconectados de la variable original. Una copia superficial significa que ciertos (sub)valores todavía están conectados a la variable original.

Para realmente entender cómo copiar, tienes que profundizar en cómo JavaScript almacena valores.

Tipos de Datos Primitivos

Tipos de datos primitivos incluyen lo siguiente:

  • Number — ej. 1
  • String — ej 'Hola'
  • Boolean — ej. true
  • undefined
  • null

Cuando creas estos valores, están estrechamente acoplados con la variable al que son asignados. Solamente existen una vez. Eso significa que no necesitas preocuparte sobre copiar tipos de datos primitivos en JavaScript. Cuando haces una copia, será una copia real. Veamos un ejemplo:

const a = 5
let b = a // esta es la copia
b = 6

console.log(b) // 6
console.log(a) // 5

Al ejecutar b = a, haces la copia. Ahora, cuando reasignas un nuevo valor a b, el valor de b cambia, pero no el de a.

Tipos de datos compuestos — Objetos y Arreglos

Técnicamente, los arreglos también son objetos, así que se comportan de la misma manera. Luego veremos ambos en detalle.

Aquí se pone más interesante. Estos valores en realidad se almacenan sólo una vez cuando se instancian, y asignar ese valor a una variable solo crea un puntero (referencia).

Ahora, si hacemos una copia b = a, y cambiamos algún valor anidado en b, en realidad cambia el valor anidado de a también, ya que a y b en realidad apuntan a la misma cosa.

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

En el ejemplo de arriba, en realidad hicimos una copia superficial. Esto es a menudo problemático, ya que esperamos que la variable anterior tenga los valores originales, no los modificados. Cuando queremos acceder a ellos, a veces obtenemos un error. Podría pasar que intentes depurarlo por un momento antes de que encuentres el error, ya que muchos desarrolladores realmente no comprenden el concepto y no esperan que eso sea el error.

1*niSsr1dxva2RWXrvHT8hxg
Photo by Thomas Millot on Unsplash

Veamos cómo podemos hacer copias de objetos y arreglos.

Objetos

Hay múltiples formas de hacer copias de objetos, especialmente con la nueva especificación de JavaScript en expansión y mejora.

Operador de propagación

Introducido con ES2015, este operador es genial, porque es muy corto y simple. 'Propaga' todos los valores en un nuevo objeto. Puedes usarlo de la siguiente manera:

const a = {
  en: 'Bye',
  de: 'Tschüss'
}

let b = {...a}
b.de = 'Ciao'

console.log(b.de) // Ciao
console.log(a.de) // Tschüss

Puedes también usarlo para fusionar dos objetos juntos, por ejemplo const c = {...a, ...b}.

Object.assign

Este fue mayormente usado antes de que el operador de propagación apareciera, y básicamente hace la misma cosa. Aunque tienes que tener cuidado, ya que el primer argumento en el método Object.assign() en realidad es modificado y devuelto. Así que asegúrate de que pases el objeto a copiar al menos como segundo argumento. Normalmente, solo pasarías un objeto vacío como primer argumento para evitar modificar cualquier dato existente.

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

Trampa: Objetos anidados

Como se mencionó antes, hay un gran peligro cuando se trata con copias de objetos, los cuales aplican a ambos métodos alistados arriba. Cuando tienes un objeto anidado (o un arreglo) y lo copias, los objetos anidados dentro de ese objeto no serán copiados, ya que ellos son solamente punteros / referencias. Por lo tanto, si cambias el objeto anidado, lo cambiarás para ambas instancias, es decir, terminarías haciendo una copia superficial otra vez.

Ejemplo:// MAL EJEMPLO

const a = {
  comidas: {
    cena: 'Pasta'
  }
}

let b = {...a}

b.comidas.cena = 'Sopa' // cambia para ambos objetos

console.log(b.comidas.cena) // Sopa
console.log(a.comidas.cena) // Sopa

Para hacer una copia profunda de objetos anidados, tendrías que considerar eso. Una manera de prevenir eso es copiar manualmente todos los objetos anidados:

const a = {
  comidas: {
    cena: 'Pasta'
  }
}

let b = {comidas: {...a.comidas}}

b.comidas.cena = 'Sopa'

console.log(b.comidas.cena) // Sopa
console.log(a.comidas.cena) // Pasta

En caso de que te estuvieras preguntado qué hacer cuando los objetos tiene más claves que solamente comidas, puedes usar todo el potencial del operador de propagación. Cuando se pasan más propiedades después de ...propagar, sobreescriben los valores originales, por ejemplo const b = {...a, comidas: {...a.comidas}}.

Haciendo copias profundas sin pensar

¿Qué pasa si no sabes cuán profundos son las estructuras anidadas? Puede ser muy tedioso iterar manualmente sobre objetos grandes y copiar cada objeto anidado a mano. Hay una manera de copiar todo sin pensar. Simplemente, usas stringify para tu objeto y luego usas parse:

const a = {
  comidas: {
    cena: 'Pasta'
  }
}


let b = JSON.parse(JSON.stringify(a))
b.comidas.cena = 'Sopa'

console.log(b.comidas.cena) // Sopa
console.log(a.comidas.cena) // Pasta

Aquí, tienes que considerar que no serás capaz de copiar instancias de clase personalizadas, así que solamente puedes usarlo cuando copias objetos por dentro con valores de JavaScript nativos.

1*hepu5hNOaAqnhE60z-oicA
Photo by Robert Zunikoff on Unsplash

Arreglos

Copiar arreglos es tan común como copiar objetos. Mucho de la lógica detrás de esto es similar, ya que los arreglos son también sólo objetos.

Operador de propagación

Así como con objetos, puedes usar el operador de propagación para copiar un arreglo:

const a = [1,2,3]
let b = [...a]

b[1] = 4

console.log(b[1]) // 4
console.log(a[1]) // 2

Funciones de arreglo — map, filter, reduce

Estos métodos regresarán un nuevo arreglo con todos (o algunos) valores del original. Mientras se hace eso, puedes también modificar los valores, lo cual viene muy bien:

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

b[1] = 4

console.log(b[1]) // 4
console.log(a[1]) // 2

Alternativamente, puedes cambiar el elemento deseado mientras copias:

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

Array.slice

Este método es normalmente usado para regresar un subconjunto de los elementos, empezando en un índice específico y opcionalmente terminando en un índice específico del arreglo original. Cuando se usa array.slice() o array.slice(0) terminarás con una copia del arreglo original.

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

b[1] = 4

console.log(b[1]) // 4
console.log(a[1]) // 2

Arreglos anidados

Similar a los objetos, usar los métodos de arriba para copiar un arreglo con otro arreglo u objeto dentro generará una copia superficial. Para prevenir eso, también usa JSON.parse(JSON.stringify(algunArreglo)).

BONUS: copiar instancia de clases personalizados

Cuando ya sos un pro en JavaScript y lidias con tus funciones constructoras personalizadas o clases, capaz quieras copiar instancias de esos también.

Como se mencionó antes, no puedes sólo usar stringify + parse, ya que perderás tus métodos de clase. En vez, querrás agregar un método copiar personalizado para crear nuevas instancias con todos los valores viejos. Veamos cómo funciona eso:

class Contador {
  constructor() {
    this.recuento = 5
  }
  copiar() {
    const copia = new Contador()
      copia.recuento = this.recuento
        return copia
  }
}

const contadorOriginal = new Contador()
const contadorCopiado = contadorOriginal.copiar()

console.log(contadorOriginal.recuento) // 5
console.log(contadorCopiado.recuento) // 5

contadorCopiado.recuento = 7

console.log(contadorOriginal.recuento) // 5
console.log(contadorCopiado.recuento) // 7

Para tratar con objetos y arreglos que son referenciados dentro de tu instancia, tendrías que aplicar tus nuevas habilidades adquiridas sobre ¡copia profunda! Añadiré sólo una solución final para el método constructor personalizado copiar para hacerlo más dinámico.

Con ese método copiar, puedes poner tantos valores como quieras en tu constructor, ¡sin tener que copiar todo manualmente!

Sobre el Autor: Lukas Gisder-Dubé co-fundó y dirigió una startup como CTO por 1 1/2 años, construyendo el equipo tech y arquitectura. Después de dejar la startup, enseñó a programar como Instructor Principal en Ironhack y ahora está construyendo una Startup de Agencia & Consultoría en Berlín. Echa un vistazo a dube.io para saber más.

1*p-l0Cee1IHvX0RQkVTOceQ