Aquí tienes un ejemplo:
var edad = 49
La variable edad (por cierto, también podrías usar let o const) almacena un valor numérico. El número 49.
Los valores numéricos se llaman "valores primitivos" porque son bloques de construcción muy simples de las aplicaciones de JavaScript.
Otros bloques de construcción simples son:
var nombre = 'Fede' // ¡los strings también son primitivas!
var esHombre = true // ¡los booleanos también lo son!
Así que números, strings y booleanos — probablemente ya te resultan muy conocidos. undefined y null son tipos primitivos adicionales.
Ya aprendimos qué son los "Primitivos" (o "tipos primitivos").
¿Qué son entonces los "tipos por referencia"?
¡Objetos y Arrays!
var persona = {
nombre: 'Fede',
edad: 49,
}
var hobbies = ['Deportes', 'Cocinar']
Aquí, persona es un objeto y por lo tanto un tipo por referencia. Por favor observa que tiene propiedades que, a su vez, tienen valores primitivos.
Sin embargo, esto no afecta al hecho de que el objeto sea un tipo por referencia. Y, por supuesto, también podrías tener objetos o arrays anidados dentro del objeto persona.
El array hobbies también es un tipo por referencia — en este caso, contiene una lista de strings. Un string es un valor / tipo primitivo, como aprendiste, pero esto no afecta al array. Los arrays son siempre tipos por referencia.
Genial, tenemos dos tipos diferentes de valores. ¿Cuál es la idea detrás de todo esto?
Está relacionado con la gestión de la memoria.
Detrás de escena, JavaScript, por supuesto, tiene que almacenar los valores que asignas a propiedades o variables en la memoria.
JavaScript maneja dos tipos de memoria: la Stack (Pila) y el Heap (Montón).
Aquí tienes un resumen súper breve: La pila es esencialmente una memoria fácil de acceder que simplemente gestiona sus elementos como una — bueno — pila. Solo elementos cuyo tamaño se conoce de antemano pueden ir en la pila. Esto es el caso de números, strings y booleanos.
El montón es una memoria para elementos cuyo tamaño y estructura exactos no puedes predecir. Dado que los objetos y arrays pueden ser mutados y cambiar en tiempo de ejecución, tienen que ir al montón.
Obviamente, hay más detalles, pero esta diferenciación general alcanza por ahora.
Para cada elemento del montón, se almacena en la pila una dirección exacta en un puntero que apunta al elemento en el montón. Esto será importante en un segundo.
Bien, tenemos memorias diferentes. Pero, ¿cómo nos afecta eso como desarrolladores?
¡El hecho de que solo los punteros se almacenen en la pila para tipos por referencia importa mucho!
¿Qué está realmente almacenado en la variable persona en el siguiente fragmento?
var persona = { nombre: 'Fede' }
¿Es:
La respuesta es (2). Un puntero al objeto persona se almacena en la variable. Lo mismo sucedería con el array hobbies.
¿Qué muestra entonces el siguiente código?
var persona = { nombre: 'Fede' }
var nuevaPersona = persona
nuevaPersona.nombre = 'Lorenzo'
console.log(persona.nombre) // ¿Qué imprime esta línea?
¡Verás 'Lorenzo' en la consola!
¿Por qué?
Porque nunca copiaste el objeto persona en sí mismo a nuevaPersona. ¡Solo copiaste el puntero! Sin embargo, todavía apunta a la misma dirección en memoria. Por eso, cambiar nuevaPersona.nombre también cambia persona.nombre, porque nuevaPersona apunta exactamente al mismo objeto.
¡Esto es realmente importante de entender! Estás apuntando al mismo objeto, no copiaste el objeto.
Es lo mismo para los arrays:
var hobbies = ['Deportes', 'Cocinar']
var hobbiesCopiados = hobbies
hobbiesCopiados .push('Música')
console.log(hobbies[2]) // ¿Qué imprime esta línea?
Esto imprime 'Música' — por la misma razón explicada antes.
Ahora que sabemos que solo copiamos el puntero — ¿cómo podemos copiar realmente el valor detrás del puntero? ¿El objeto o array real?
Básicamente necesitas construir un nuevo objeto o array y llenarlo inmediatamente con las propiedades o elementos del objeto o array antiguo.
Tienes múltiples maneras de hacer esto — dependiendo también de qué versión de JavaScript estés usando durante el desarrollo.
slice() es un método estándar de array provisto por JavaScript. Puedes consultar su documentación completa aquí.
var hobbies = ['Deportes', 'Cocinar']
var hobbiesCopiados = hobbies.slice()
Básicamente devuelve un nuevo array que contiene todos los elementos del array original, comenzando desde el índice inicial que hayas pasado (y hasta el máximo número de elementos que hayas definido). Si simplemente llamas slice() sin argumentos, obtienes un nuevo array con todos los elementos del array original.
Si estás usando ES6+, puedes usar el operador de propagación.
var hobbies = ['Deportes', 'Cocinar']
var hobbiesCopiados = [...hobbies]
Aquí también creas un nuevo array (manualmente, usando []) y luego usas el operador de propagación ... para "extraer todos los elementos del array antiguo" y agregarlos al nuevo array.
var persona = { nombre: 'Fede' }
var personaCopiada = Object.assign({}, persona)
Esta sintaxis crea un nuevo objeto (la parte {}) y asigna todas las propiedades del objeto antiguo (el segundo argumento) a ese nuevo objeto. Esto crea una copia.
var persona = { nombre: 'Fede' }
var personaCopiada = { ...persona }
Esto también creará un nuevo objeto (porque usaste {}) y luego extraerá todas las propiedades de persona hacia el nuevo objeto.
Ahora sabes cómo clonar arrays y objetos.
Sin embargo, aquí hay algo súper importante que debes notar: ¡No estás creando clonaciones profundas con ninguno de estos métodos!
Si el array clonado contiene arrays anidados u objetos como elementos, o si tu objeto contiene propiedades que contienen arrays u otros objetos, entonces estos arrays u objetos anidados no habrán sido clonados.
Todavía tendrás los punteros antiguos, apuntando a los arrays / objetos anidados antiguos.
Tendrías que clonar manualmente cada capa que planees modificar. Si no planeas cambiar esos arrays u objetos anidados, no necesitas clonarlos.