Cómo diferenciar entre copias profundas y superficiales en JavaScript

¡Lo nuevo siempre es mejor!

Seguramente ha tratado con copias en JavaScript antes, incluso si no lo sabía. Quizás también hayas oído hablar del paradigma en la programación funcional de que no debes modificar ningún dato existente. Para hacer eso, debe saber cómo copiar valores de forma segura en JavaScript. ¡Hoy veremos cómo hacer esto evitando las trampas!

En primer lugar, ¿qué es una copia?

Una copia se parece a la anterior, pero no lo es. Cuando cambia la copia, espera que el original permanezca igual, mientras que la copia cambia.

En programación, almacenamos valores en variables. Hacer una copia significa que inicia una nueva variable con los mismos valores. Sin embargo, existe un gran peligro potencial a considerar: copia profunda versus copia superficial . Una copia profunda significa que todos los valores de la nueva variable se copian y desconectan de la variable original . Una copia superficial significa que ciertos (sub) valores todavía están conectados a la variable original.

Para comprender realmente la copia, debe conocer cómo almacena JavaScript los valores.

Tipos de datos primitivos

Los tipos de datos primitivos incluyen los siguientes:

  • Número - por ejemplo 1
  • Cadena - por ejemplo 'Hello'
  • Booleano - p. Ej. true
  • undefined
  • null

Cuando crea estos valores, están estrechamente relacionados con la variable a la que están asignados. Solo existen una vez. Eso significa que realmente no tiene que preocuparse por copiar tipos de datos primitivos en JavaScript. Cuando haga una copia, será una copia real. Veamos un ejemplo:

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

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

Tipos de datos compuestos: objetos y matrices

Técnicamente, las matrices también son objetos, por lo que se comportan de la misma manera. Revisaré ambos en detalle más adelante.

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

Ahora, si hacemos una copia b = ay cambiamos algún valor anidado b, en realidad también cambia ael valor anidado, ya que ay en brealidad apuntan a lo mismo. Ejemplo:

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 anterior, en realidad hicimos una copia superficial . A menudo, esto es problemático, ya que esperamos que la variable anterior tenga los valores originales, no los modificados. Cuando accedemos a él, a veces recibimos un error. Puede suceder que intente depurarlo durante un tiempo antes de encontrar el error, ya que muchos desarrolladores no comprenden realmente el concepto y no esperan que ese sea el error.

Echemos un vistazo a cómo podemos hacer copias de objetos y matrices.

Objetos

Hay varias formas de hacer copias de objetos, especialmente con la nueva especificación de JavaScript que se expande y mejora.

Operador de propagación

Introducido con ES2015, este operador es simplemente genial, porque es muy corto y simple. Se 'extiende' todos los valores en un nuevo objeto. Puede 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

También puede usarlo para fusionar dos objetos, por ejemplo const c = {...a, ...b}.

Object.assign

Esto se usó principalmente antes de que existiera el operador de propagación, y básicamente hace lo mismo. Sin embargo, debe tener cuidado, ya que el primer argumento del Object.assign()método en realidad se modifica y se devuelve. Así que asegúrese de pasar el objeto a copiar al menos como segundo argumento. Normalmente, solo pasaría un objeto vacío como primer argumento para evitar modificar los datos 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

Dificultad: objetos anidados

Como se mencionó anteriormente, hay una gran advertencia cuando se trata de copiar objetos, que se aplica a los dos métodos enumerados anteriormente. Cuando tiene un objeto anidado (o matriz) y lo copia, los objetos anidados dentro de ese objeto no se copiarán, ya que son solo punteros / referencias. Por lo tanto, si cambia el objeto anidado, lo cambiará para ambas instancias, lo que significa que terminaría haciendo una copia superficial nuevamente . Ejemplo: // MAL EJEMPLO

const a = {
 foods: {
 dinner: 'Pasta'
 }
}
let b = {...a}
b.foods.dinner = 'Soup' // changes for both objects
console.log(b.foods.dinner) // Soup
console.log(a.foods.dinner) // Soup

Para hacer una copia profunda de los objetos anidados , debería considerar eso. Una forma de evitarlo es copiar manualmente todos los objetos anidados:

const a = {
 foods: {
 dinner: 'Pasta'
 }
}
let b = {foods: {...a.foods}}
b.foods.dinner = 'Soup'
console.log(b.foods.dinner) // Soup
console.log(a.foods.dinner) // Pasta

En caso de que se pregunte qué hacer cuando el objeto tiene más claves que solo foods, puede utilizar todo el potencial del operador de propagación. Al pasar más propiedades después de ...spread, sobrescriben los valores originales, por ejemplo const b = {...a, foods: {...a.foods}}.

Hacer copias profundas sin pensar

¿Qué pasa si no sabe qué tan profundas son las estructuras anidadas? Puede ser muy tedioso revisar manualmente objetos grandes y copiar todos los objetos anidados a mano. Hay una forma de copiar todo sin pensar. Simplemente stringifytu objeto y parsejusto después:

const a = {
 foods: {
 dinner: 'Pasta'
 }
}
let b = JSON.parse(JSON.stringify(a))
b.foods.dinner = 'Soup'
console.log(b.foods.dinner) // Soup
console.log(a.foods.dinner) // Pasta

Aquí, debe considerar que no podrá copiar instancias de clases personalizadas, por lo que solo puede usarlas cuando copie objetos con valores JavaScript nativos en su interior.

Matrices

Copiar matrices es tan común como copiar objetos. Gran parte de la lógica detrás de esto es similar, ya que las matrices también son solo objetos debajo del capó.

Operador de propagación

Al igual que con los objetos, puede usar el operador de extensión para copiar una matriz:

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

Funciones de matriz: mapear, filtrar, reducir

Estos métodos devolverán una nueva matriz con todos (o algunos) valores de la original. Mientras hace eso, también puede modificar los valores, lo que es muy ú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

Alternativamente, puede cambiar el elemento deseado mientras copia:

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

Array.slice

Este método se usa normalmente para devolver un subconjunto de los elementos, comenzando en un índice específico y opcionalmente terminando en un índice específico de la matriz original. Cuando use array.slice()o array.slice(0)terminará con una copia de la matriz original.

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

Matrices anidadas

De manera similar a los objetos, usar los métodos anteriores para copiar una matriz con otra matriz u objeto dentro generará una copia superficial . Para evitar eso, también use JSON.parse(JSON.stringify(someArray)).

BONUS: copia de instancia de clases personalizadas

Cuando ya sea un profesional en JavaScript y se ocupe de sus funciones o clases de constructor personalizadas, tal vez desee copiar instancias de esas también.

Como se mencionó anteriormente, no puede simplemente secuenciar + analizar esos, ya que perderá sus métodos de clase. En su lugar, querrá agregar un copymétodo personalizado para crear una nueva instancia con todos los valores anteriores. Veamos cómo funciona eso:

class Counter {
 constructor() {
 this.count = 5
 }
 copy() {
 const copy = new Counter()
 copy.count = this.count
 return copy
 }
}
const originalCounter = new Counter()
const copiedCounter = originalCounter.copy()
console.log(originalCounter.count) // 5
console.log(copiedCounter.count) // 5
copiedCounter.count = 7
console.log(originalCounter.count) // 5
console.log(copiedCounter.count) // 7

Para tratar con objetos y matrices a los que se hace referencia dentro de su instancia, ¡tendría que aplicar sus habilidades recién aprendidas sobre copia profunda ! Solo agregaré una solución final para el copymétodo de constructor personalizado para hacerlo más dinámico:

Con ese método de copia, puede poner tantos valores como desee en su constructor, ¡sin tener que copiar todo manualmente!

Sobre el autor: Lukas Gisder-Dubé cofundó y dirigió una startup como CTO durante 1 año y medio, creando el equipo de tecnología y la arquitectura. Después de dejar la startup, enseñó codificación como instructor principal en Ironhack y ahora está construyendo una agencia de startups y consultoría en Berlín. Visite dube.io para obtener más información.