Escuela Frontend
JavaScript

Diferencias Entre Valor y Referencia en JavaScript

Claudia Valdivieso
Autora
Claudia Valdivieso

Entender cómo funcionan los valores y las referencias cuando asignamos variables o las pasamos como parámetro es un concepto clave si quieres crecer como desarrollador(a) front-end. Ademas, dominar este tema te ayudara a evitar muchos de los bugs que son causados por no entender lo que está pasando con las variables de nuestros programas.

¿Qué aprenderemos?

En este artículo vamos a aprender sobre los valores y las referencias en JavaScript:

  • Cómo se almacenan las variables en la memoria de la computadora.
  • Qué pasa cuando asignamos una variable.
  • Qué pasa cuando pasamos una variable como parámetro en una función

Tipos de Datos en JavaScript

JavaScript tiene 5 tipos de datos que son pasados por valor: Boolean, Null, Undefined, String y Number, los cuales son valores Primitivos del lenguaje.

También, JavaScript tiene un tipo de dato que es pasado por referencia: Object.

Valores Primitivos

Si un valor de tipo primitivo es asignado a una variable, podemos pensar que esa variable contiene el valor primitivo.

let x = 10;
let y = 'abc';
let z = null;

Con base a lo anterior, podemos decir que: x contiene 10 & y contiene 'abc'.

Para consolidar esta idea, revisaremos una imagen de cómo se ven estas variables y sus respectivos valores en la memoria.

https://res.cloudinary.com/escuela-frontend/image/upload/v1631635588/articles/diferencias-valores-referencias/Valor_vs_referencia1_bopuzu.png

Cuando asignamos estas variables a otras variables usando =, copiamos el valor a la nueva variable, es decir, se copian por valor.

let x = 10;
let y = 'abc';
let a = x;
let b = y;
console.log(x, y, a, b); // -> 10, 'abc', 10, 'abc'

Tanto a como x contienen ahora 10. Tanto b como y contienen ahora 'abc'.

Están separados, ya que los valores mismos fueron copiados.

https://res.cloudinary.com/escuela-frontend/image/upload/v1631635588/articles/diferencias-valores-referencias/Valor_vs_referencia2_rogkjk.png

Cambiar una no cambia la otra porque las variables no tienen ninguna relación entre sí.

let x = 10;
let y = 'abc';
let a = x;
let b = y;
a = 5;
b = 'def';
console.log(x, y, a, b); // -> 10, 'abc', 5, 'def'

Objetos

Esto te parecerá un poco confuso, pero se paciente y léelo con detenimiento. Una vez que lo entiendas, te parecerá fácil.

💡Las variables a las que se les asigna un valor no primitivo reciben una referencia a ese valor. Esa referencia apunta a la ubicación del objeto en la memoria. Las variables no contienen realmente el valor💡

Los objetos se crean en algún lugar de la memoria de tu ordenador. Cuando escribimos arr = [], hemos creado un array en memoria. Lo que recibe la variable arr es la dirección, la ubicación, de ese array.

Imaginemos que dicha direccion es un nuevo tipo de dato que pasa por valor, como un number o un string. Una direccion apunta a la ubicación, en memoria, de un valor que se pasa por referencia. Al igual que una cadena se denota con comillas ('' o ""), denotaremos direccion con una x adelante, por ejemplo: x123, que será la ubicación en memoria.

Cuando asignamos y utilizamos una variable de tipo referencia, lo que escribimos y vemos es:

let arr = []; // paso 1
arr.push(1); // paso 2

Paso 1

https://res.cloudinary.com/escuela-frontend/image/upload/v1631635588/articles/diferencias-valores-referencias/Valor_vs_referencia3_hssl4f.png

Paso 2

https://res.cloudinary.com/escuela-frontend/image/upload/v1631635588/articles/diferencias-valores-referencias/Valor_vs_referencia4_pdr1ip.png

Observa que el valor, es decir la dirección, que contiene la variable arr es estática. El array en memoria es lo que cambia. Cuando usamos arr para hacer algo, como agregar un valor, el motor de JavaScript va a la ubicación de arr en la memoria y trabaja con la información almacenada allí.

Asignación por referencia

Cuando un valor de tipo referencia, por ejemplo un objeto, es copiado a otra variable usando =, la dirección de ese valor es lo que realmente se copia como si fuera en forma primitiva. Los objetos se copian por referencia en lugar de por valor.

let arr = [1];
let arr2 = arr;

El código anterior se ve así en la memoria.

https://res.cloudinary.com/escuela-frontend/image/upload/v1631635588/articles/diferencias-valores-referencias/Valor_vs_referencia5_ntagra.png

Cada variable contiene ahora una referencia al mismo array. Esto significa que si alteramos la variable arr, arr2 también será alterada:

arr.push(2);
console.log(arr, arr2); // -> [1, 2], [1, 2]

Hemos agregado 2 en el array en memoria. Cuando usamos arr y arr2, estamos apuntando a ese mismo array.

https://res.cloudinary.com/escuela-frontend/image/upload/v1631635588/articles/diferencias-valores-referencias/Valor_vs_referencia6_d2jhgu.png

Reasignación de una referencia

La reasignación de una variable de referencia sustituye a la antigua referencia.

let obj = { primero: 'referencia' };

En memoria:

https://res.cloudinary.com/escuela-frontend/image/upload/v1631635589/articles/diferencias-valores-referencias/Valor_vs_referencia7_crqcfg.png

Luego agregamos

obj = { segundo: 'ref2' }

La dirección almacenada en obj cambia. El primer objeto sigue presente en la memoria, y también el siguiente:

https://res.cloudinary.com/escuela-frontend/image/upload/v1631635589/articles/diferencias-valores-referencias/Valor_vs_referencia8_rtmcdz.png

Cuando no quedan referencias a un objeto, como vemos para la dirección x234 arriba, el motor de JavaScript puede realizar la recolección de basura. Esto sólo significa que el programador ha perdido todas las referencias al objeto y ya no puede utilizarlo, por lo que el motor puede seguir adelante y borrarlo de la memoria de forma segura. En este caso, el objeto { primero: 'referencia' } ya no es accesible y está disponible para el motor para la recolección de basura.

== y ===

Cuando los operadores de igualdad, == y ===, se utilizan en variables de tipo referencia, comprueban la referencia. Si las variables contienen una referencia al mismo elemento, la comparación resultará verdadera.

let arrRef = ['Hola'];
let arrRef2 = arrRef;
console.log(arrRef === arrRef2); // -> true

Si son objetos distintos, aunque contengan propiedades idénticas, la comparación resultará falsa.

let arr1 = ['Hola'];
let arr2 = ['Hola'];
console.log(arr1 === arr2); // -> false

Si tenemos dos objetos distintos y queremos ver si sus propiedades son iguales, la forma más fácil de hacerlo es convertirlos a ambos en cadenas y luego comparar las cadenas. Cuando los operadores de igualdad comparan primitivas, simplemente comprueban si los valores son iguales.

Aunque esto no es muy fiable, sobre todo cuando las propiedades del objeto no están en el mismo orden.

Una solución más fiable sería usar recursividad en los objetos a comparar para asegurarte de que cada propiedad es la misma y contiene el mismo valor.

Pasar parámetros a través de funciones

Cuando pasamos valores primitivos a una función, ésta copia los valores en sus parámetros. Es efectivamente lo mismo que usar =.

let cien = 100;
let dos = 2;
function multiplicar(x, y) {
// PAUSA
return x * y;
}
let doscientos = multiplicar(cien, dos);

En el ejemplo anterior, damos a cien el valor 100. Cuando lo pasamos a multiplicar, la variable x obtiene ese valor, 100. El valor se copia como si usáramos una asignación =. De nuevo, el valor de cien no se ve afectado. Esta es una instantánea de cómo se ve la memoria justo en la línea de comentario PAUSA en multiplicar.

https://res.cloudinary.com/escuela-frontend/image/upload/v1631635589/articles/diferencias-valores-referencias/Valor_vs_referencia9_hu8rfm.png

Mientras una función sólo tome valores primitivos como parámetros y no utilice ninguna variable en su ámbito circundante, es llamada una función pura, ya que no afecta a nada en el ámbito exterior. Todas las variables creadas en su interior son eliminadas por el recolector de basura tan pronto como la función retorna.

Sin embargo, una función que toma un objeto, puede mutar el estado de su ámbito circundante. Si una función toma una referencia a un array y altera el array al que apunta, quizás agregando un elemento, las variables del ámbito circundante que hacen referencia a ese array perciben ese cambio. Después de que la función regresa, los cambios que hace persisten en el ámbito externo. Esto puede causar efectos secundarios no deseados que pueden ser difíciles de rastrear.

Muchas funciones nativas de los arrays, incluyendo Array.map y Array.filter, están escritas como funciones puras. Toman una referencia a un array e internamente copian el array y trabajan con la copia en lugar del original. Esto hace que el original no sea tocado, que el ámbito externo no se vea afectado, y que se nos devuelva una referencia a un array completamente nuevo.

Pasemos a un ejemplo de una función pura vs impura.

function cambiandoLaEdadImpuramente(persona) {
persona.edad = 25;
return persona;
}
let claudia = {
nombre: 'Claudia',
edad: 31
};
let claudiaCambiada = cambiandoLaEdadImpuramente(claudia);
console.log(claudia); // -> { nombre: 'Claudia', edad: 25 }
console.log(claudiaCambiada); // -> { nombre: 'Claudia', age: 25 }

Esta función impura toma un objeto y cambia la propiedad edad de ese objeto para que sea 25. Como actúa sobre la referencia que se le dio, cambia directamente el objeto claudia. Observa que cuando devuelve el objeto persona, está devolviendo exactamente el mismo objeto que se le pasó. claudia y claudiaCambiada contienen la misma referencia. Es redundante devolver la variable persona y almacenar la referencia en una nueva variable.

Veamos una función pura.

function cambiandoLaEdadPuramente(persona) {
let nuevaPersona = JSON.parse(JSON.stringify(persona));
nuevaPersona.age = 25;
return nuevaPersona;
}
let claudia = {
nombre: 'Claudia',
edad: 31
};
let claudiaCambiada = cambiandoLaEdadPuramente(claudia);
console.log(claudia); // -> { nombre: 'Claudia', edad: 31 }
console.log(claudiaCambiada); // -> { nombre: 'Claudia', edad: 25 }

En esta función, utilizamos JSON.stringify para transformar el objeto que se nos pasa en una cadena, y luego lo volvemos a parsear en un objeto con JSON.parse. Al realizar esta transformación y almacenar el resultado en una nueva variable, hemos creado un nuevo objeto. Hay otras formas de hacer lo mismo que veremos en un siguiente artículo, pero esta forma es la más sencilla. El nuevo objeto tiene las mismas propiedades que el original pero es un objeto distinto en la memoria.

Comprobemos si aprendimos

Valor vs referencia es un concepto que se pone a prueba a menudo en las entrevistas. Intenta averiguar qué se muestra aquí.

function cambiaLaEdadYLaReferencia(persona) {
persona.edad = 25;
persona = {
nombre: 'John',
edad: 50
};
return persona;
}
let persona1 = {
nombre: 'Claudia',
edad: 31
};
let persona2 = cambiaLaEdadYLaReferencia(persona1);
console.log(persona1); // -> ?
console.log(persona2); // -> ?

.

.

.

.

.

.

.

.

.

.

La función primero cambia la edad de la propiedad en el objeto original que se le pasó. Luego reasigna la variable a un objeto nuevo y devuelve ese objeto. Esto es lo que lo que se imprime:

console.log(persona1); // -> { nombre: 'Claudia', edad: 25 }
console.log(persona2); // -> { nombre: 'John', edad: 50 }

Recuerda que la asignación a través de los parámetros de la función es esencialmente lo mismo que la asignación con =. La variable persona en la función contiene una referencia al objeto persona1, por lo que inicialmente actúa directamente sobre ese objeto. Una vez que reasignamos persona a un nuevo objeto, deja de afectar al original.

Esta reasignación no cambia el objeto al que apunta persona1 en el ámbito externo. persona tiene una nueva referencia porque fue reasignada pero esta reasignación no cambia persona1.

Un fragmento de código equivalente al bloque anterior sería:

let persona1 = {
nombre: 'Claudia',
edad: 31
};
let persona = persona1;
persona.age = 25;
persona = {
nombre: 'John',
edad: 50
};
var persona2 = persona;
console.log(persona1); // -> { nombre: 'Claudia', edad: 25 }
console.log(persona2); // -> { nombre: 'John', edad: 50 }

La única diferencia es que cuando usamos la función, persona deja de estar en el ámbito una vez que la función termina.

Resumen

En JavaScript los tipos primitivos se pasan como valores: lo que significa que cada vez que se asigna un valor, se crea una copia de ese valor.

Por otro lado los objetos (incluyendo objetos planos, arrays, funciones, instancias de clases) son referencias. Si modificas el objeto, entonces todas las variables que hacen referencia a ese objeto van a ver el cambio.

El operador de comparación distingue entre valores y referencias. Dos variables que tienen referencias son iguales sólo si hacen referencia exactamente al mismo objeto, pero dos variables que tienen valores son iguales si simplemente tienen dos valores iguales sin importar el origen del valor.

Por último, cuando pasas una variable como parámetro de una función, si esta tiene un valor primitivo, el valor se copia a la variable interna de la función. Y si es un objeto, la variable de la función tomará la referencia a dicho objeto. Sea que el valor de la variable sea primitiva o sea un objeto, lo mejor es usar funciones puras, ya que solo operan en su ámbito y no alteran ningún valor fuera de ellas.

Otros recursos

  • Este video tiene una muy buena explicación de este tema que me ayudó mucho para este artículo.
  • También te recomiendo este curso de Dan Abramov que te explica este concepto aunque con un modelo mental diferente pero muy muy comprensible.

Artículos Relacionados

¿Quieres mejorar tus habilidades de frontend?