Saltar al contenido principal

[Medium] 📄 Primitive vs Reference Types

1. What are Primitive Types and Reference Types?​

¿Qué son los tipos primitivos (Primitive Types) y los tipos de referencia (Reference Types)?

Los tipos de datos en JavaScript se dividen en dos grandes categorías: tipos primitivos y tipos de referencia. Tienen diferencias fundamentales en cómo se almacenan en memoria y cómo se comportan al ser pasados.

Tipos primitivos (Primitive Types)​

Características:

  • Se almacenan en el Stack (pila)
  • Al pasarlos se copia el valor en sí (Call by Value)
  • Son inmutables (Immutable)

Incluyen 7 tipos:

// 1. String (cadena de texto)
const str = 'hello';

// 2. Number (número)
const num = 42;

// 3. Boolean (booleano)
const bool = true;

// 4. Undefined
let undef;

// 5. Null
const n = null;

// 6. Symbol (ES6)
const sym = Symbol('unique');

// 7. BigInt (ES2020)
const bigInt = 9007199254740991n;

Tipos de referencia (Reference Types)​

Características:

  • Se almacenan en el Heap (montículo)
  • Al pasarlos se copia la referencia (dirección de memoria) (Call by Reference)
  • Son mutables (Mutable)

Incluyen:

// 1. Object (objeto)
const obj = { name: 'John' };

// 2. Array (arreglo)
const arr = [1, 2, 3];

// 3. Function (función)
const func = function () {};

// 4. Date
const date = new Date();

// 5. RegExp
const regex = /abc/;

// 6. Map, Set, WeakMap, WeakSet (ES6)
const map = new Map();
const set = new Set();

2. Call by Value vs Call by Reference​

Paso por valor (Call by Value) vs Paso por referencia (Call by Reference)

Paso por valor (Call by Value) - Tipos primitivos​

Comportamiento: Se copia el valor en sí; modificar la copia no afecta al valor original.

// Tipo primitivo: paso por valor
let a = 10;
let b = a; // Copiar valor

b = 20; // Modificar b

console.log(a); // 10 (sin efecto)
console.log(b); // 20

Diagrama de memoria:

┌─────────┐
│ Stack │
├─────────┤
│ a: 10 │ ← Valor independiente
├─────────┤
│ b: 20 │ ← Valor independiente (modificado después de copiar)
└─────────┘

Paso por referencia (Call by Reference) - Tipos de referencia​

Comportamiento: Se copia la dirección de memoria; dos variables apuntan al mismo objeto.

// Tipo de referencia: paso por referencia
let obj1 = { name: 'John' };
let obj2 = obj1; // Copiar dirección de memoria

obj2.name = 'Jane'; // Modificar a través de obj2

console.log(obj1.name); // 'Jane' (¡afectado!)
console.log(obj2.name); // 'Jane'
console.log(obj1 === obj2); // true (apuntan al mismo objeto)

Diagrama de memoria:

┌─────────┐ ┌──────────────────┐
│ Stack │ │ Heap │
├─────────┤ ├──────────────────┤
│ obj1 ───┼───────────────────>│ { name: 'Jane' } │
├─────────┤ │ │
│ obj2 ───┼───────────────────>│ (mismo objeto) │
└─────────┘ └──────────────────┘

3. Common Quiz Questions​

Preguntas frecuentes de examen

Pregunta 1: Paso de tipos primitivos​

function changeValue(x) {
x = 100;
console.log('x dentro de la función:', x);
}

let num = 50;
changeValue(num);
console.log('num fuera de la función:', num);
Haz clic para ver la respuesta
// x dentro de la función: 100
// num fuera de la función: 50

Explicación:

  • num es un tipo primitivo (Number)
  • Al pasarlo a la función se copia el valor, x y num son variables independientes
  • Modificar x no afecta a num
// Flujo de ejecución
let num = 50; // Stack: num = 50
changeValue(num); // Stack: x = 50 (copia)
x = 100; // Stack: x = 100 (solo modifica x)
console.log(num); // Stack: num = 50 (sin efecto)

Pregunta 2: Paso de tipos de referencia​

function changeObject(obj) {
obj.name = 'Changed';
console.log('obj.name dentro de la función:', obj.name);
}

let person = { name: 'Original' };
changeObject(person);
console.log('person.name fuera de la función:', person.name);
Haz clic para ver la respuesta
// obj.name dentro de la función: Changed
// person.name fuera de la función: Changed

Explicación:

  • person es un tipo de referencia (Object)
  • Al pasarlo a la función se copia la dirección de memoria
  • obj y person apuntan al mismo objeto
  • Modificar el contenido del objeto a través de obj también afecta a person
// Diagrama de memoria
let person = { name: 'Original' }; // Heap: crear objeto @0x001
changeObject(person); // Stack: obj = @0x001 (copiar dirección)
obj.name = 'Changed'; // Heap: @0x001.name = 'Changed'
console.log(person.name); // Heap: @0x001.name (mismo objeto)

Pregunta 3: Reasignación vs modificación de propiedades​

function test1(obj) {
obj.name = 'Modified'; // Modificar propiedad
}

function test2(obj) {
obj = { name: 'New Object' }; // Reasignar
}

let person = { name: 'Original' };

test1(person);
console.log('A:', person.name);

test2(person);
console.log('B:', person.name);
Haz clic para ver la respuesta
// A: Modified
// B: Modified (¡no es 'New Object'!)

Explicación:

test1: Modificación de propiedad

function test1(obj) {
obj.name = 'Modified'; // ✅ Modifica la propiedad del objeto original
}
// person y obj apuntan al mismo objeto, por lo que se modifica

test2: Reasignación

function test2(obj) {
obj = { name: 'New Object' }; // ❌ Solo cambia la referencia de obj
}
// obj ahora apunta a un nuevo objeto, pero person sigue apuntando al original

Diagrama de memoria:

// Antes de test1
person ────> { name: 'Original' }
obj ────> { name: 'Original' } (el mismo)

// Después de test1
person ────> { name: 'Modified' }
obj ────> { name: 'Modified' } (el mismo)

// Ejecución de test2
person ────> { name: 'Modified' } (sin cambio)
obj ────> { name: 'New Object' } (nuevo objeto)

// Después de test2
person ────> { name: 'Modified' } (sigue sin cambio)
// obj se destruye, el nuevo objeto es recolectado por el garbage collector

Pregunta 4: Paso de arreglos​

function modifyArray(arr) {
arr.push(4);
console.log('1:', arr);
}

function reassignArray(arr) {
arr = [5, 6, 7];
console.log('2:', arr);
}

let numbers = [1, 2, 3];
modifyArray(numbers);
console.log('3:', numbers);

reassignArray(numbers);
console.log('4:', numbers);
Haz clic para ver la respuesta
// 1: [1, 2, 3, 4]
// 3: [1, 2, 3, 4]
// 2: [5, 6, 7]
// 4: [1, 2, 3, 4]

Explicación:

  • modifyArray: Modifica el contenido del arreglo original, numbers se ve afectado
  • reassignArray: Solo cambia la referencia del parámetro, numbers no se ve afectado

Pregunta 5: Operaciones de comparación​

// Comparación de tipos primitivos
let a = 10;
let b = 10;
console.log('A:', a === b);

// Comparación de tipos de referencia
let obj1 = { value: 10 };
let obj2 = { value: 10 };
let obj3 = obj1;
console.log('B:', obj1 === obj2);
console.log('C:', obj1 === obj3);
Haz clic para ver la respuesta
// A: true
// B: false
// C: true

Explicación:

Tipos primitivos: Comparan valores

10 === 10; // true (mismo valor)

Tipos de referencia: Comparan direcciones de memoria

obj1 === obj2; // false (objetos diferentes, direcciones diferentes)
obj1 === obj3; // true (apuntan al mismo objeto)

Diagrama de memoria:

obj1 ────> @0x001: { value: 10 }
obj2 ────> @0x002: { value: 10 } (mismo contenido pero dirección diferente)
obj3 ────> @0x001: { value: 10 } (misma dirección que obj1)

4. Shallow Copy vs Deep Copy​

Copia superficial vs Copia profunda

Copia superficial (Shallow Copy)​

Definición: Solo copia el primer nivel; los objetos anidados siguen siendo referencias.

Método 1: Operador de propagación (Spread Operator)​

const original = {
name: 'John',
address: { city: 'Taipei' },
};

const copy = { ...original };

// Modificar primer nivel: no afecta al objeto original
copy.name = 'Jane';
console.log(original.name); // 'John' (sin efecto)

// Modificar objeto anidado: ¡afecta al objeto original!
copy.address.city = 'Kaohsiung';
console.log(original.address.city); // 'Kaohsiung' (¡afectado!)

Método 2: Object.assign()​

const original = { name: 'John', age: 30 };
const copy = Object.assign({}, original);

copy.name = 'Jane';
console.log(original.name); // 'John' (sin efecto)

Método 3: Copia superficial de arreglos​

const arr1 = [1, 2, 3];

// Método 1: Operador de propagación
const arr2 = [...arr1];

// Método 2: slice()
const arr3 = arr1.slice();

// Método 3: Array.from()
const arr4 = Array.from(arr1);

arr2[0] = 999;
console.log(arr1[0]); // 1 (sin efecto)

Copia profunda (Deep Copy)​

Definición: Copia completamente todos los niveles, incluyendo objetos anidados.

Método 1: JSON.parse + JSON.stringify (el más común)​

const original = {
name: 'John',
address: { city: 'Taipei' },
hobbies: ['reading', 'gaming'],
};

const copy = JSON.parse(JSON.stringify(original));

// Modificar objeto anidado: no afecta al objeto original
copy.address.city = 'Kaohsiung';
console.log(original.address.city); // 'Taipei' (sin efecto)

copy.hobbies.push('coding');
console.log(original.hobbies); // ['reading', 'gaming'] (sin efecto)

Limitaciones:

const obj = {
date: new Date(), // ❌ Se convierte en string
func: () => {}, // ❌ Se ignora
undef: undefined, // ❌ Se ignora
symbol: Symbol('test'), // ❌ Se ignora
regexp: /abc/, // ❌ Se convierte en {}
circular: null, // ❌ Referencias circulares causan error
};
obj.circular = obj; // Referencia circular

JSON.parse(JSON.stringify(obj)); // Error o pérdida de datos

Método 2: structuredClone() (navegadores modernos)​

const original = {
name: 'John',
address: { city: 'Taipei' },
date: new Date(),
};

const copy = structuredClone(original);

// Puede copiar correctamente objetos especiales como Date
console.log(copy.date instanceof Date); // true

Ventajas:

  • ✅ Soporta Date, RegExp, Map, Set, etc.
  • ✅ Soporta referencias circulares
  • ✅ Mejor rendimiento

Limitaciones:

  • ❌ No soporta funciones
  • ❌ No soporta Symbol

Método 3: Implementación recursiva de copia profunda​

function deepClone(obj) {
// Manejar null y no-objetos
if (obj === null || typeof obj !== 'object') {
return obj;
}

// Manejar arreglos
if (Array.isArray(obj)) {
return obj.map((item) => deepClone(item));
}

// Manejar Date
if (obj instanceof Date) {
return new Date(obj);
}

// Manejar RegExp
if (obj instanceof RegExp) {
return new RegExp(obj);
}

// Manejar objetos
const cloned = {};
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
cloned[key] = deepClone(obj[key]);
}
}

return cloned;
}

// Ejemplo de uso
const original = {
name: 'John',
address: { city: 'Taipei' },
hobbies: ['reading'],
date: new Date(),
};

const copy = deepClone(original);
copy.address.city = 'Kaohsiung';
console.log(original.address.city); // 'Taipei' (sin efecto)

Método 4: Usar Lodash​

import _ from 'lodash';

const original = {
name: 'John',
address: { city: 'Taipei' },
};

const copy = _.cloneDeep(original);

Comparación: Copia superficial vs Copia profunda​

CaracterísticaCopia superficialCopia profunda
Niveles copiadosSolo primer nivelTodos los niveles
Objetos anidadosSiguen siendo referenciasCompletamente independientes
RendimientoRápidoLento
MemoriaPocaMucha
Caso de usoObjetos simplesEstructuras anidadas complejas

5. Common Pitfalls​

Trampas comunes

Trampa 1: Creer que pasar parámetros puede cambiar tipos primitivos​

// ❌ Entendimiento incorrecto
function increment(num) {
num = num + 1;
return num;
}

let count = 5;
increment(count);
console.log(count); // 5 (¡no se convierte en 6!)

// ✅ Forma correcta
count = increment(count); // Necesita recibir el valor de retorno
console.log(count); // 6

Trampa 2: Creer que reasignar puede cambiar el objeto externo​

// ❌ Entendimiento incorrecto
function resetObject(obj) {
obj = { name: 'Reset' }; // Solo cambia la referencia del parámetro
}

let person = { name: 'Original' };
resetObject(person);
console.log(person.name); // 'Original' (¡no se reseteó!)

// ✅ Forma correcta 1: Modificar propiedades
function resetObject(obj) {
obj.name = 'Reset';
}

// ✅ Forma correcta 2: Retornar nuevo objeto
function resetObject(obj) {
return { name: 'Reset' };
}
person = resetObject(person);

Trampa 3: Creer que el operador de propagación es una copia profunda​

// ❌ Entendimiento incorrecto
const original = {
user: { name: 'John' },
};

const copy = { ...original }; // ¡Copia superficial!

copy.user.name = 'Jane';
console.log(original.user.name); // 'Jane' (¡fue afectado!)

// ✅ Forma correcta: Copia profunda
const copy = JSON.parse(JSON.stringify(original));
// o
const copy = structuredClone(original);

Trampa 4: Malentendido sobre const​

// ¡const solo impide la reasignación, no la inmutabilidad!

const obj = { name: 'John' };

// ❌ No se puede reasignar
obj = { name: 'Jane' }; // TypeError: Assignment to constant variable

// ✅ Se pueden modificar propiedades
obj.name = 'Jane'; // Funciona normalmente
obj.age = 30; // Funciona normalmente

// Para verdadera inmutabilidad
const immutableObj = Object.freeze({ name: 'John' });
immutableObj.name = 'Jane'; // Falla silenciosamente (en modo estricto lanza error)
console.log(immutableObj.name); // 'John' (no fue modificado)

Trampa 5: Problema de referencias en bucles​

// ❌ Error común
const arr = [];
const obj = { value: 0 };

for (let i = 0; i < 3; i++) {
obj.value = i;
arr.push(obj); // ¡Todos apuntan al mismo objeto!
}

console.log(arr);
// [{ value: 2 }, { value: 2 }, { value: 2 }]
// Todos son el mismo objeto, el valor final es 2

// ✅ Forma correcta: Crear nuevo objeto cada vez
const arr = [];

for (let i = 0; i < 3; i++) {
arr.push({ value: i }); // Crear nuevo objeto cada vez
}

console.log(arr);
// [{ value: 0 }, { value: 1 }, { value: 2 }]

6. Best Practices​

Mejores prácticas

✅ Métodos recomendados​

// 1. Cuando necesites copiar objetos, usa métodos de copia explícitos
const original = { name: 'John', age: 30 };

// Copia superficial (objetos simples)
const copy1 = { ...original };

// Copia profunda (objetos anidados)
const copy2 = structuredClone(original);

// 2. Las funciones no deben depender de efectos secundarios para modificar parámetros
// ❌ Mal
function addItem(arr, item) {
arr.push(item); // Modifica el arreglo original
}

// ✅ Bien
function addItem(arr, item) {
return [...arr, item]; // Retorna nuevo arreglo
}

// 3. Usar const para prevenir reasignación accidental
const config = { theme: 'dark' };
// config = {}; // Lanzará error

// 4. Usar Object.freeze cuando se necesiten objetos inmutables
const constants = Object.freeze({
PI: 3.14159,
MAX_SIZE: 100,
});

❌ Métodos a evitar​

// 1. No depender del paso de parámetros para modificar tipos primitivos
function increment(num) {
num++; // ❌ Sin efecto
}

// 2. No confundir copia superficial con copia profunda
const copy = { ...nested }; // ❌ Creer que es copia profunda

// 3. No reutilizar la misma referencia de objeto en bucles
for (let i = 0; i < 3; i++) {
arr.push(obj); // ❌ Todos apuntan al mismo objeto
}

7. Interview Summary​

Resumen para entrevistas

Memorización rápida​

Tipos primitivos (Primitive):

  • String, Number, Boolean, Undefined, Null, Symbol, BigInt
  • Paso por valor (Call by Value)
  • Almacenados en Stack
  • Inmutables (Immutable)

Tipos de referencia (Reference):

  • Object, Array, Function, Date, RegExp, etc.
  • Paso por referencia (Call by Reference)
  • Almacenados en Heap
  • Mutables (Mutable)

Ejemplo de respuesta en entrevista​

Q: ¿JavaScript es Call by Value o Call by Reference?

JavaScript es Call by Value para todos los tipos, pero el "valor" que se pasa para los tipos de referencia es la dirección de memoria.

  • Tipos primitivos: Se pasa una copia del valor, las modificaciones no afectan al valor original
  • Tipos de referencia: Se pasa una copia de la dirección, a través de la dirección se puede modificar el objeto original
  • Sin embargo, si se reasigna (se cambia la dirección), no afecta al objeto original

Reference​