Nivel inicial14 min de lectura · Publicado el

Paso por referencia y paso por valor en JavaScript: copias

Referencias y copias en JavaScript: un primitivo se copia como un papel independiente; equipo1 y equipo2 guardan la dirección de la misma casa. Valor o referencia en JSrubentejera.com valorreferenciaPepeAledos copiasindependientesequipo1equipo2la misma casa
Un primitivo es un valor apuntado en un papel: al copiarlo hay dos papeles independientes. Un objeto es una casa: equipo1 y equipo2 guardan la misma dirección y, si uno pinta la puerta (equipo1[0] = "X"), el otro la ve pintada.

El paso por valor y el paso por referencia explican por qué, en JavaScript, a veces cambias un array y cambia también otro que no has tocado. La regla corta: los primitivos se copian y los objetos se comparten. Cuando necesitas dos objetos independientes, tienes que copiarlo tú, y esta guía explica también cómo.

La imagen de la portada lo resume. Un primitivo es un dato apuntado en un papel: si lo copias en otro papel, tienes dos papeles y puedes tachar uno sin tocar el otro. Un objeto es una casa, y lo que guarda la variable es un papel con su dirección. Si copias ese papel, tienes dos papeles con la misma dirección, pero sigue habiendo una sola casa: si alguien pinta la puerta, la verás pintada vayas con el papel que vayas.

Los ejemplos de JavaScript se han ejecutado con Node 24.22.2 y Node 26.13.0, con la misma salida en los dos, que aparece debajo de cada uno. El de C++, con g++ 16.2.0.

En esta guía:

Qué significan paso por valor y paso por referencia

Son dos formas de dar un dato a una función (o a otra variable):

  • Paso por valor: la función recibe una copia. Lo que haga con ella no afecta a la variable original.
  • Paso por referencia: la función recibe la propia variable, no una copia. Si la cambia, cambia también fuera.

Algunos lenguajes, como C++, permiten elegir. Este ejemplo en C++ lo muestra con una sola diferencia, el &:

#include <iostream>

void porValor(int numero)       { numero = 99; } // cambia una copia
void porReferencia(int& numero) { numero = 99; } // cambia la variable de fuera

int main() {
    int a = 1;
    int b = 1;
    porValor(a);
    porReferencia(b);
    std::cout << a << " " << b << "\n";
}

Compilado con g++ 16.2.0:

1 99

JavaScript no tiene ese &: no hay forma de que una función reciba la variable de fuera. Lo que sí ocurre es que, cuando el dato es un objeto, la copia que recibe es la dirección de la casa. Por eso a menudo parece paso por referencia, como se ve a continuación.

Con primitivos se copia el valor

Los primitivos de JavaScript son string, number, bigint, boolean, undefined, null y symbol. Todo lo demás (arrays, funciones, fechas, objetos literales…) es un objeto.

let persona1 = "Ana";
let persona2 = "Ale";

persona1 = persona2; // persona1 recibe una copia de "Ale"
persona1 = "Pepe";   // cambia solo persona1

console.log(persona1, persona2);
Pepe Ale

persona2 no cambia: son dos papeles distintos.

Con objetos y arrays se copia la referencia

let equipo1 = ["a", "b", "c"];
let equipo2 = ["d", "e", "f"];

equipo1 = equipo2; // las dos variables apuntan al mismo array
equipo1[0] = "X";  // se modifica ese array

console.log(equipo1, equipo2);
console.log(equipo1 === equipo2);
[ 'X', 'e', 'f' ] [ 'X', 'e', 'f' ]
true

Es el mismo movimiento que antes, pero ahora cambian los dos. Al hacer equipo1 = equipo2 no se copió el contenido del array, sino su dirección. El array ["a", "b", "c"] se queda sin nadie que apunte a él, y el recolector de basura de JavaScript podrá liberar esa memoria.

Lo mismo pasa al llamar a una función

Un parámetro funciona como una variable nueva a la que se asigna lo que le pasas. Las reglas son las mismas:

function cambia(persona, numero) {
  persona.nombre = "Pepe"; // modifica el objeto recibido
  numero = 99;             // cambia solo la copia local
}

const persona = { nombre: "Ale" };
let numero = 1;
cambia(persona, numero);

console.log(persona, numero);
{ nombre: 'Pepe' } 1

La función pintó la puerta de la casa (persona.nombre) y todos lo ven. En cambio, el número era un papel suyo: tacharlo no cambia el de fuera.

Por qué se dice que JavaScript siempre pasa por valor

En sentido estricto, JavaScript siempre copia el valor de la variable. MDN lo explica así (resumen nuestro): los parámetros se pasan, en esencia, por valor, y si la función cambia las propiedades de un objeto recibido, ese cambio se ve fuera. En un objeto, el valor que se copia es la referencia.

La diferencia se nota al intentar sustituir el objeto dentro de la función:

function reasigna(persona) {
  persona = { nombre: "Otro" }; // solo cambia la variable local
}

const persona = { nombre: "Pepe" };
reasigna(persona);
console.log(persona);
{ nombre: 'Pepe' }

Dentro de la función, persona era un papel con la dirección de la casa. Escribir otra dirección en ese papel no cambia el papel de fuera ni la casa. Con paso por referencia real, como el int& de C++ o ref en C#, esa asignación sí cambiaría la variable de fuera.

En la práctica, quédate con dos cosas:

Dentro de la función… Con un primitivo Con un objeto o array
Cambias su contenido (p.nombre = …, lista.push(…)) No se puede: los primitivos no tienen contenido que cambiar Se ve fuera
Reasignas el parámetro (p = …) No se ve fuera No se ve fuera

const no impide cambiar un objeto

Una confusión frecuente es pensar que const hace que un objeto no se pueda modificar. No es así: const impide reasignar la variable, es decir, escribir otra dirección en el papel. La casa sigue pudiendo cambiar.

const equipo = ["a", "b"];
equipo.push("c");      // cambia el contenido: permitido
equipo[0] = "X";       // también permitido
console.log(equipo);

try {
  equipo = ["otro"];   // reasignar la variable: prohibido
} catch (error) {
  console.log(error.constructor.name + ": " + error.message);
}

const edad = 30;
try {
  edad = 31;           // con un primitivo, const lo impide todo
} catch (error) {
  console.log(error.constructor.name + ": " + error.message);
}
[ 'X', 'b', 'c' ]
TypeError: Assignment to constant variable.
TypeError: Assignment to constant variable.

Con un primitivo, const sí deja el dato fijo, porque la única forma de cambiarlo es reasignar. Con un objeto, solo fija qué objeto es. Según MDN, const crea una referencia que no se puede reasignar, lo que no significa que el valor que guarda sea inmutable.

Entonces, ¿para qué sirve const con objetos? Para que nadie cambie por error a qué objeto apunta la variable. Y sigue siendo útil para quien lee: sabe que equipo será el mismo array durante toda la función. Las demás diferencias entre const, let y var están en var, let y const en JavaScript.

Si quieres que el objeto no cambie: Object.freeze

Para impedir que se modifique el contenido, existe Object.freeze(). En modo estricto (el de los módulos y las clases), escribir en un objeto congelado lanza un error; fuera de él, el cambio se ignora sin avisar.

"use strict";

const config = Object.freeze({ tema: "oscuro", idiomas: ["es"] });

try {
  config.tema = "claro"; // en modo estricto lanza un error
} catch (error) {
  console.log(error.constructor.name + ": " + error.message);
}

config.idiomas.push("en"); // freeze es superficial: el array interior no se congela
console.log(config);
console.log(Object.isFrozen(config), Object.isFrozen(config.idiomas));
TypeError: Cannot assign to read only property 'tema' of object '#<Object>'
{ tema: 'oscuro', idiomas: [ 'es', 'en' ] }
true false

Fíjate en la última línea: freeze congela la casa, pero no las casas a las que apuntan sus propiedades. idiomas es otro objeto y sigue pudiendo cambiar. Tienes más sobre freeze en qué es un objeto en JavaScript.

Comparar objetos: === compara direcciones

Como la variable guarda una dirección, === entre objetos comprueba si son la misma casa, no si están amueblados igual:

const a = { nombre: "Ana" };
const b = a;                 // la misma casa
const c = { nombre: "Ana" }; // otra casa amueblada igual

console.log(a === b);
console.log(a === c);
true
false

Si necesitas comparar el contenido, tienes que hacerlo propiedad a propiedad o con una utilidad de comparación profunda. Las reglas de ==, === y Object.is están en diferencias entre == y === en JavaScript.

Copiar un array en JavaScript

Ya has visto que equipo1 = equipo2 no copia el array: deja dos papeles con la misma dirección. Copiar es hacer una fotocopia de la lista de la compra en lugar de compartirla: si tachas algo en la tuya, la otra sigue igual.

Hay cinco formas habituales:

Forma Código Profundidad
Spread (ES2015) [...equipo2] Superficial
slice() equipo2.slice() Superficial
concat() [].concat(equipo2) Superficial
Array.from() Array.from(equipo2) Superficial
structuredClone() structuredClone(equipo2) Profunda

Las cuatro primeras hacen lo mismo con un array sencillo:

const equipo2 = ["d", "e", "f"];

const conSpread = [...equipo2];       // la forma más corta y la más usada hoy
const conSlice = equipo2.slice();     // sin argumentos, copia todo el array
const conConcat = [].concat(equipo2); // une el original a un array vacío
const conFrom = Array.from(equipo2);  // crea un array nuevo a partir del original

conSpread[0] = "X";
console.log(equipo2, conSpread);
console.log(conSlice, conConcat, conFrom);

// Array.from acepta además una función para transformar cada elemento al copiar
console.log(Array.from(equipo2, (letra) => letra.toUpperCase()));
[ 'd', 'e', 'f' ] [ 'X', 'e', 'f' ]
[ 'd', 'e', 'f' ] [ 'd', 'e', 'f' ] [ 'd', 'e', 'f' ]
[ 'D', 'E', 'F' ]

Cambiar la copia no toca equipo2. Pero las cuatro copian solo el primer nivel. Si el array tiene otros arrays u objetos dentro, la copia guarda sus direcciones y los comparte con el original:

const anidado = [["a"], ["b"]];
const superficial = [...anidado];
superficial[0][0] = "X";
console.log(anidado); // ¡ha cambiado el original!

const otro = [["a"], ["b"]];
const profunda = structuredClone(otro);
profunda[0][0] = "X";
console.log(otro, profunda);
[ [ 'X' ], [ 'b' ] ]
[ [ 'a' ], [ 'b' ] ] [ [ 'X' ], [ 'b' ] ]

Para una copia completa de un array anidado, usa structuredClone(). Está en los navegadores principales desde 2022 y en Node desde la versión 17. Si dentro hay funciones, falla: lo ves en la sección siguiente.

Clonar un objeto en JavaScript

Con un objeto pasa lo mismo: copia = original no clona nada. Estas son las opciones:

Método Profundidad Funciones Fechas, Map, Set
{ ...obj } Superficial Las copia Comparte las de dentro
Object.assign({}, obj) Superficial Las copia Comparte las de dentro
JSON.parse(JSON.stringify(obj)) Profunda Las pierde Fechas a texto; Map y Set pasan a {}
structuredClone(obj) Profunda Error DataCloneError Las respeta
Función recursiva propia Profunda Las copia Depende de cómo la escribas

Spread y Object.assign: copia superficial

const persona = { nombre: "Luis", edad: 20 };

const conSpread = { ...persona };
const conAssign = Object.assign({}, persona);

// Copiar y cambiar a la vez: lo de la derecha pisa a lo de la izquierda
const cambiada = { ...persona, nombre: "Juan" };
const cambiadaAssign = Object.assign({}, persona, { edad: 40, pelo: "castaño" });

conSpread.nombre = "Pepe";
console.log(persona);
console.log(conSpread, conAssign);
console.log(cambiada, cambiadaAssign);
{ nombre: 'Luis', edad: 20 }
{ nombre: 'Pepe', edad: 20 } { nombre: 'Luis', edad: 20 }
{ nombre: 'Juan', edad: 20 } { nombre: 'Luis', edad: 40, pelo: 'castaño' }

En Object.assign, el primer argumento es el objeto destino y los siguientes se copian encima, de izquierda a derecha. Spread es hoy la forma más habitual. Hay una diferencia sutil: Object.assign ejecuta los setters del objeto destino y spread no, porque crea las propiedades directamente. Con un destino vacío, como aquí, el resultado es el mismo.

Copia superficial y copia profunda

Las dos copian solo el primer nivel. Volviendo a las casas: hacen una casa nueva con los mismos muebles, pero si dentro había un papel con la dirección de otra casa, copian el papel, no esa otra casa.

const persona = {
  nombre: "Luis",
  social: { instagram: "@luis" },
};

const copia = { ...persona }; // o Object.assign({}, persona)
persona.social.instagram = "@luis_nuevo";

console.log(copia.social.instagram);
console.log(copia.social === persona.social);
@luis_nuevo
true

Hemos cambiado persona y también ha cambiado copia: el objeto social no se copió, se copió su dirección. Una copia profunda copia también esas casas interiores, y así hasta el final.

Copia superficial frente a copia profunda del objeto config, con tema: "oscuro" e idiomas: ["es"]. Con { ...config }, config y la copia apuntan al mismo array ["es"], que se comparte. Con structuredClone(config), también se copia el array: cada uno tiene el suyo.
Con el objeto config del ejemplo de Object.freeze. Cada flecha es una dirección: con { ...config }, las dos apuntan a la misma casa, el array idiomas; con structuredClone(config), cada objeto tiene la suya.

structuredClone: la copia profunda actual

structuredClone() hace una copia profunda de verdad. Respeta fechas, Map y Set, y también las referencias circulares, en las que un objeto se apunta a sí mismo, directa o indirectamente:

const datos = {
  nombre: "Luis",
  social: { instagram: "@luis" },
  alta: new Date(0),
  etiquetas: new Set(["js"]),
};
datos.yoMismo = datos; // referencia circular

const copia = structuredClone(datos);
console.log(copia.social === datos.social);
console.log(copia.alta instanceof Date, copia.etiquetas.has("js"));
console.log(copia.yoMismo === copia);
false
true true
true

Tiene dos límites. El primero son las funciones: si el objeto tiene un método, lanza un DataCloneError.

const conMetodo = {
  nombre: "Luis",
  saluda() {
    return "Hola";
  },
};

try {
  structuredClone(conMetodo);
} catch (error) {
  console.log(error.name + ": " + error.message);
}
DataCloneError: saluda() {
    return "Hola";
  } could not be cloned.

El segundo es el prototipo, que no se copia: si clonas una instancia de una clase, obtienes un objeto plano, sin sus métodos. Tampoco se conservan los getters y setters (se copia su valor), las propiedades no enumerables ni los campos privados (#x).

class Punto {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }
  distancia() {
    return Math.hypot(this.x, this.y);
  }
}

const clon = structuredClone(new Punto(3, 4));
console.log(clon, clon instanceof Punto, typeof clon.distancia);
{ x: 3, y: 4 } false undefined

JSON.parse y JSON.stringify: el truco de antes

Antes de structuredClone, la copia profunda habitual era pasar el objeto a texto JSON y de vuelta. Sigue funcionando, pero pierde por el camino todo lo que JSON no sabe guardar:

const original = {
  nombre: "Luis",
  social: { instagram: "@luis" },
  alta: new Date(0),
  etiquetas: new Set(["js"]),
  saluda() {
    return "Hola";
  },
};

const copia = JSON.parse(JSON.stringify(original));
console.log(copia);
{
  nombre: 'Luis',
  social: { instagram: '@luis' },
  alta: '1970-01-01T00:00:00.000Z',
  etiquetas: {}
}

social es independiente, pero la fecha se ha convertido en texto, el Set en un objeto vacío y saluda() ha desaparecido. Tampoco conserva undefined, y con una referencia circular lanza un error. Hoy es mejor structuredClone.

Una función recursiva, si hay funciones dentro

Si tu objeto tiene métodos y necesitas una copia profunda, puedes escribir la tuya. Esta trata aparte null (porque typeof null es "object") y los arrays, dos fallos típicos de las versiones rápidas:

function deepClone(valor) {
  if (typeof valor !== "object" || valor === null) {
    return valor; // primitivos y funciones se devuelven tal cual
  }
  const copia = Array.isArray(valor) ? [] : {};
  for (const clave of Object.keys(valor)) {
    copia[clave] = deepClone(valor[clave]);
  }
  return copia;
}

const persona = {
  nombre: "Luis",
  edad: 20,
  social: { instagram: "@luis" },
  etiquetas: ["js"],
  sinDato: null,
  getEdad() {
    return this.edad;
  },
};

const copia = deepClone(persona);
persona.social.instagram = "@otro";

console.log(copia.social.instagram, copia.getEdad());
console.log(Array.isArray(copia.etiquetas), copia.sinDato);
@luis 20
true null

Es sencilla a propósito: no copia fechas, Map, Set ni el prototipo, y con una referencia circular se llama a sí misma sin fin hasta lanzar RangeError: Maximum call stack size exceeded. Si necesitas todo eso, usa structuredClone o una librería como cloneDeep de Lodash.

Qué forma de copiar elegir

  1. ¿Es un array u objeto de un solo nivel (textos, números)? Usa [...array] o { ...objeto }.
  2. ¿Tiene arrays u objetos dentro y ninguna función? Usa structuredClone().
  3. ¿Tiene funciones o es una instancia de una clase? Plantéate separar datos y comportamiento; si no puedes, una función recursiva o Lodash.

Otra salida es no tener que copiar: si un objeto no se puede modificar, compartirlo es seguro. Es la idea de los value objects, objetos inmutables que se comparan por lo que valen.

Errores típicos

Una función que modifica lo que recibe

Si una función cambia un array o un objeto que le pasan, ese cambio llega a quien la llamó, aunque no lo espere. Es más predecible devolver uno nuevo:

// Versión que modifica el array que recibe
function anadeNotaMutando(notas, nota) {
  notas.push(nota);
  return notas;
}

// Versión que devuelve un array nuevo y deja el original como estaba
function anadeNota(notas, nota) {
  return [...notas, nota];
}

const originales = [7, 9];

const nuevas = anadeNota(originales, 5);
console.log(originales, nuevas);

anadeNotaMutando(originales, 5);
console.log(originales);
[ 7, 9 ] [ 7, 9, 5 ]
[ 7, 9, 5 ]

Modificar no es siempre un error: a veces es justo lo que quieres, y copiar un array enorme en cada paso tiene un coste. Lo importante es que el nombre o la documentación de la función lo dejen claro.

Métodos que cambian el array original

Algunos métodos de array modifican el propio array y además lo devuelven, así que es fácil creer que tienes una copia:

const notas = [7, 3, 9];

const ordenadas = notas.sort((a, b) => a - b); // ordena el mismo array y lo devuelve
console.log(notas, ordenadas, notas === ordenadas);

const otras = [7, 3, 9];
const copiaOrdenada = otras.toSorted((a, b) => a - b); // devuelve un array nuevo
console.log(otras, copiaOrdenada);
[ 3, 7, 9 ] [ 3, 7, 9 ] true
[ 7, 3, 9 ] [ 3, 7, 9 ]

La función (a, b) => a - b hace que se ordenen como números: sin ella, sort() compara como texto, y [10, 9, 1].sort() da [ 1, 10, 9 ].

Pasa lo mismo con reverse() y splice(), que tienen versiones que no modifican: toReversed() y toSpliced(). Los tienes todos en métodos de arrays en JavaScript.

Si dos variables comparten objeto, comparten también los cambios.

Vídeo: clonado de objetos, de Carlos Blé

Fuentes

Consultadas el 11 de noviembre de 2026:

Los ejemplos de JavaScript se han ejecutado con Node 24.22.2 y Node 26.13.0 (imágenes oficiales de Docker node:24.22.2 y node:26.13.0), y el de C++ con g++ 16.2.0 en la imagen oficial de Docker gcc. Esta guía reúne tres artículos de 2018: este, «Clonado de arrays en JavaScript» y «Clonado de objetos en JavaScript», que mostraban la salida con capturas de la consola del navegador; aquí se sustituyen por la salida de Node en texto.