Explorando los nuevos métodos inmutables de array en JavaScript (ES2023)



<p>
    El ecosistema de JavaScript nunca deja de evolucionar, y con cada nueva versión de ECMAScript, los desarrolladores recibimos herramientas más potentes y refinadas para escribir código más limpio, predecible y robusto. La versión ES2023 (también conocida como ES14) no es la excepción, trayendo consigo una serie de adiciones significativas que buscan mejorar la experiencia de manipulación de datos, especialmente cuando trabajamos con arrays. De entre estas novedades, los nuevos métodos inmutables de array destacan como un cambio que, aunque sutil en su concepto, tiene un impacto profundo en cómo abordamos la gestión del estado y la lógica de negocio.
</p>
<p>
    Durante mucho tiempo, la manipulación de arrays en JavaScript ha sido un arma de doble filo. Métodos como <code>sort()</code>, <code>reverse()</code> o <code>splice()</code> son increíblemente útiles, pero conllevan una característica inherente que a menudo causa dolores de cabeza: mutan el array original. Esto puede llevar a efectos secundarios inesperados, especialmente en aplicaciones complejas donde los arrays pueden ser compartidos entre diferentes partes del código o cuando se trabaja con frameworks que dependen de la inmutabilidad para la detección de cambios (como React o Vue). Este tutorial detallado, que incluye ejemplos de código, explorará a fondo estos nuevos métodos inmutables, desglosando su funcionamiento, sus casos de uso y cómo pueden transformar la forma en que escribimos JavaScript. Prepárense para abrazar un paradigma de programación más seguro y predecible.
</p>

<h2>Introducción a la inmutabilidad en JavaScript</h2><img src="https://images.pexels.com/photos/2004161/pexels-photo-2004161.jpeg?auto=compress&cs=tinysrgb&h=650&w=940" alt="Vivid, blurred close-up of colorful code on a screen, representing web development and programming."/>

<p>
    Antes de sumergirnos en los nuevos métodos, es fundamental comprender qué es la inmutabilidad y por qué es tan valiosa. En el contexto de la programación, un dato es inmutable si su estado no puede ser modificado después de su creación. Si necesitamos cambiar un dato inmutable, en realidad creamos una nueva copia con las modificaciones deseadas, dejando el original intacto.
</p>
<p>
    Este principio contrasta directamente con la mutabilidad, donde el dato original se modifica directamente. Si bien la mutabilidad puede parecer más "directa" y, en teoría, más eficiente al evitar la creación de nuevas estructuras de datos, a menudo introduce complejidad:
</p>
<ul>
    <li>
        <strong>Efectos secundarios inesperados:</strong> Cuando un array se pasa a una función y esta lo muta, otras partes del código que aún referencian el array original pueden ver su estado alterado sin previo aviso, lo que dificulta el seguimiento de los cambios y la depuración.
    </li>
    <li>
        <strong>Depuración más compleja:</strong> Rastrear el origen de un error se vuelve un desafío cuando el estado de los datos puede cambiar en cualquier momento y lugar.
    </li>
    <li>
        <strong>Manejo del estado:</strong> En arquitecturas de aplicaciones modernas, como las que utilizan Redux o los contextos de React, la inmutabilidad es clave para una detección eficiente de cambios y una gestión del estado predecible.
    </li>
</ul>
<p>
    La tendencia en JavaScript moderno es moverse hacia patrones más inmutables, utilizando técnicas como el operador spread (<code>...</code>) para crear copias de arrays u objetos antes de modificarlos. Los métodos de array que vamos a explorar ahora formalizan y simplifican este patrón para operaciones comunes, haciendo que el código sea más legible y menos propenso a errores.
</p>

<h2>¿Qué son los métodos inmutables de array?</h2>

<p>
    ES2023 introduce cuatro nuevos métodos de prototipo de <code>Array</code> diseñados para realizar operaciones comunes de manipulación de arrays sin modificar el array original. En su lugar, devuelven un *nuevo* array con los cambios aplicados. Estos métodos son:
</p>
<ul>
    <li><code>Array.prototype.toReversed()</code></li>
    <li><code>Array.prototype.toSorted(compareFn)</code></li>
    <li><code>Array.prototype.toSpliced(start, deleteCount, ...items)</code></li>
    <li><code>Array.prototype.with(index, value)</code></li>
</ul>
<p>
    Cada uno de estos métodos tiene un homólogo mutable preexistente (<code>reverse()</code>, <code>sort()</code>, <code>splice()</code> y, de forma menos directa, la asignación directa por índice). La clave para entenderlos es que su propósito es el mismo, pero su comportamiento respecto a la mutación es diametralmente opuesto. Personalmente, considero que esta es una de las adiciones más sensatas y prácticas en las últimas versiones de ECMAScript, ya que estandariza un patrón que muchos desarrolladores ya implementaban manualmente.
</p>

<h3><code>Array.prototype.toReversed():</code> invirtiendo sin efectos secundarios</h3>

<p>
    El método <code>toReversed()</code> es la contraparte inmutable de <code>reverse()</code>. Mientras que <code>reverse()</code> modifica el array original invirtiendo el orden de sus elementos, <code>toReversed()</code> devuelve un *nuevo* array con los elementos en orden inverso, dejando el array original intacto.
</p>

<h4>Sintaxis:</h4>
<pre><code class="language-js">
const newArray = array.toReversed();
</code></pre>

<h4>Ejemplo de código:</h4>
<pre><code class="language-js">
const numerosOriginales = [1, 2, 3, 4, 5];

// Usando el método mutable (reverse())
const numerosMutados = [...numerosOriginales]; // Crear una copia para simular
numerosMutados.reverse();
console.log('Original después de reverse():', numerosOriginales); // [1, 2, 3, 4, 5] (si no hubiéramos copiado, ¡sería diferente!)
console.log('Array mutado:', numerosMutados); // [5, 4, 3, 2, 1]

// Usando el nuevo método inmutable (toReversed())
const numerosInvertidos = numerosOriginales.toReversed();
console.log('Original después de toReversed():', numerosOriginales); // [1, 2, 3, 4, 5] (¡intacto!)
console.log('Array invertido:', numerosInvertidos); // [5, 4, 3, 2, 1]

// Ejemplo con un caso de uso real donde la inmutabilidad es clave:
function mostrarUltimosNombres(listaNombres) {
    // Si usáramos .reverse(), la lista original se alteraría para futuros usos.
    // Con .toReversed(), la función es "pura" y no tiene efectos secundarios.
    return listaNombres.toReversed().slice(0, 3);
}

const participantes = ['Ana', 'Juan', 'María', 'Pedro', 'Sofía', 'Luis'];
const ultimosTres = mostrarUltimosNombres(participantes);

console.log('Participantes originales:', participantes); // ['Ana', 'Juan', 'María', 'Pedro', 'Sofía', 'Luis']
console.log('Últimos tres participantes:', ultimosTres); // ['Luis', 'Sofía', 'Pedro']
</code></pre>
<p>
    Como se puede apreciar, <code>toReversed()</code> nos permite obtener el array invertido sin tener que preocuparnos por copiar explícitamente el array original antes de la operación, lo que hace el código más conciso y claro. Es un pequeño pero significativo paso hacia una programación más funcional.
</p>

<h3><code>Array.prototype.toSorted(compareFn):</code> ordenando de forma segura</h3>

<p>
    El método <code>toSorted()</code> es la versión inmutable de <code>sort()</code>. Funciona de manera idéntica a <code>sort()</code> en cuanto a su lógica de ordenación (incluyendo la capacidad de pasar una función comparadora <code>compareFn</code> para un ordenamiento personalizado), pero la diferencia fundamental es que <code>toSorted()</code> devuelve un *nuevo* array ordenado, mientras que <code>sort()</code> modifica el array original.
</p>
<p>
    Personalmente, considero que la adición de <code>toSorted()</code> es una de las mejoras más significativas en la manipulación de arrays. La antigua <code>sort()</code> era una fuente común de errores inesperados y frustración, especialmente para desarrolladores menos experimentados, al mutar el array original. Ahora, podemos clasificar datos con la tranquilidad de saber que nuestra fuente de datos original permanece intacta, lo cual es invaluable en arquitecturas de aplicaciones modernas que dependen fuertemente de la inmutabilidad del estado.
</p>

<h4>Sintaxis:</h4>
<pre><code class="language-js">
const newArray = array.toSorted(compareFn);
</code></pre>

<h4>Ejemplo de código:</h4>
<pre><code class="language-js">
const productosOriginales = [
    { nombre: 'Café', precio: 10 },
    { nombre: 'Azúcar', precio: 5 },
    { nombre: 'Leche', precio: 8 },
    { nombre: 'Pan', precio: 2 }
];

// Ordenar por precio de forma ascendente
const productosOrdenadosPorPrecio = productosOriginales.toSorted((a, b) => a.precio - b.precio);

console.log('Productos originales:', productosOriginales);
/*
[
  { nombre: 'Café', precio: 10 },
  { nombre: 'Azúcar', precio: 5 },
  { nombre: 'Leche', precio: 8 },
  { nombre: 'Pan', precio: 2 }
]
(¡intacto!)
*/
console.log('Productos ordenados por precio:', productosOrdenadosPorPrecio);
/*
[
  { nombre: 'Pan', precio: 2 },
  { nombre: 'Azúcar', precio: 5 },
  { nombre: 'Leche', precio: 8 },
  { nombre: 'Café', precio: 10 }
]
*/

// Ordenar por nombre alfabéticamente
const productosOrdenadosPorNombre = productosOriginales.toSorted((a, b) => a.nombre.localeCompare(b.nombre));

console.log('Productos ordenados por nombre:', productosOrdenadosPorNombre);
/*
[
  { nombre: 'Azúcar', precio: 5 },
  { nombre: 'Café', precio: 10 },
  { nombre: 'Leche', precio: 8 },
  { nombre: 'Pan', precio: 2 }
]
*/

const palabras = ["banana", "apple", "grape"];
const palabrasOrdenadas = palabras.toSorted();

console.log("Palabras originales:", palabras); // ["banana", "apple", "grape"]
console.log("Palabras ordenadas:", palabrasOrdenadas); // ["apple", "banana", "grape"]
</code></pre>
<p>
    Este método es particularmente útil cuando se trabaja con interfaces de usuario que muestran listas de datos que pueden necesitar ser ordenadas de diferentes maneras, sin alterar la fuente de datos subyacente.
</p>

<h3><code>Array.prototype.toSpliced(start, deleteCount, ...items):</code> modificando con precisión y seguridad</h3>

<p>
    El método <code>toSpliced()</code> es la versión inmutable del versátil <code>splice()</code>. Recordemos que <code>splice()</code> es una navaja suiza que permite eliminar elementos, insertar elementos o reemplazar elementos en un array, todo ello mutando el array original. <code>toSpliced()</code> ofrece la misma funcionalidad pero devuelve un *nuevo* array con los cambios aplicados, dejando el array original intacto.
</p>
<p>
    Esta es una adición muy potente porque las operaciones que realiza <code>splice()</code> son intrínsecamente complejas de replicar de forma inmutable manualmente con el operador spread, especialmente cuando se combinan eliminaciones e inserciones.
</p>

<h4>Sintaxis:</h4>
<pre><code class="language-js">
const newArray = array.toSpliced(start, deleteCount, item1, item2, ...);
</code></pre>

<h4>Ejemplo de código:</h4>
<pre><code class="language-js">
const listaTareas = ['Comprar pan', 'Estudiar JS', 'Hacer ejercicio', 'Leer un libro'];

// 1. Eliminar un elemento
const tareasSinEstudiar = listaTareas.toSpliced(1, 1); // Eliminar 'Estudiar JS' (índice 1, 1 elemento)
console.log('Lista original:', listaTareas); // ['Comprar pan', 'Estudiar JS', 'Hacer ejercicio', 'Leer un libro']
console.log('Lista sin estudiar JS:', tareasSinEstudiar); // ['Comprar pan', 'Hacer ejercicio', 'Leer un libro']

// 2. Insertar un elemento
const tareasConNuevaTarea = listaTareas.toSpliced(2, 0, 'Llamar a María'); // En índice 2, eliminar 0, insertar 'Llamar a María'
console.log('Lista con nueva tarea:', tareasConNuevaTarea); // ['Comprar pan', 'Estudiar JS', 'Llamar a María', 'Hacer ejercicio', 'Leer un libro']

// 3. Reemplazar elementos
const tareasActualizadas = listaTareas.toSpliced(0, 1, 'Comprar leche'); // En índice 0, eliminar 1, insertar 'Comprar leche'
console.log('Lista tareas actualizadas:', tareasActualizadas); // ['Comprar leche', 'Estudiar JS', 'Hacer ejercicio', 'Leer un libro']

// 4. Combinar operaciones (eliminar y añadir)
const tareasConPrioridad = listaTareas.toSpliced(3, 1, 'Reunión de equipo', 'Preparar presentación'); // Eliminar 'Leer un libro', añadir 2 nuevas
console.log('Lista con prioridad:', tareasConPrioridad); // ['Comprar pan', 'Estudiar JS', 'Hacer ejercicio', 'Reunión de equipo', 'Preparar presentación']
</code></pre>
<p>
    Este método es increíblemente útil para la gestión de listas de elementos, como carritos de compra, listas de reproducción o flujos de trabajo en aplicaciones donde la inmutabilidad de los datos es crucial. Te recomiendo explorar más a fondo la documentación de <a href="https://developer.mozilla.org/es/docs/Web/JavaScript/Reference/Global_Objects/Array/toSpliced" target="_blank" rel="noopener noreferrer"><code>Array.prototype.toSpliced</code> en MDN</a> para ver todos sus matices.
</p>

<h3><code>Array.prototype.with(index, value):</code> actualizando elementos individualmente</h3>

<p>
    El método <code>with()</code> es una adición más específica que permite reemplazar un elemento en un índice dado con un nuevo valor, devolviendo un *nuevo* array sin modificar el original. Antes de <code>with()</code>, para lograr esto de forma inmutable, normalmente se utilizaba una combinación del operador spread y la sintaxis de array para un índice específico (<code>[...array.slice(0, index), newValue, ...array.slice(index + 1)]</code>). <code>with()</code> simplifica enormemente esta operación.
</p>
<p>
    Es importante notar que este método fue añadido en el mismo lote que los demás métodos inmutables y proporciona una sintaxis más limpia para un caso de uso muy común: la actualización de un solo elemento en un array de manera inmutable.
</p>

<h4>Sintaxis:</h4>
<pre><code class="language-js">
const newArray = array.with(index, value);
</code></pre>

<h4>Ejemplo de código:</h4>
<pre><code class="language-js">
const puntuaciones = [10, 20, 30, 40, 50];

// Actualizar la puntuación en el índice 2 (30) a 35
const nuevasPuntuaciones = puntuaciones.with(2, 35);

console.log('Puntuaciones originales:', puntuaciones); // [10, 20, 30, 40, 50] (¡intacto!)
console.log('Nuevas puntuaciones:', nuevasPuntuaciones); // [10, 20, 35, 40, 50]

// Actualizar un elemento en un array de objetos
const usuarios = [
    { id: 1, nombre: 'Ana' },
    { id: 2, nombre: 'Juan' },
    { id: 3, nombre: 'María' }
];

// Actualizar el nombre del usuario con id 2
// Primero, encontrar el índice
const indiceJuan = usuarios.findIndex(u => u.id === 2);
const usuarioActualizado = { ...usuarios[indiceJuan], nombre: 'Juan Manuel' }; // Crear una copia del objeto
const usuariosActualizados = usuarios.with(indiceJuan, usuarioActualizado);

console.log('Usuarios originales:', usuarios);
/*
[
  { id: 1, nombre: 'Ana' },
  { id: 2, nombre: 'Juan' },
  { id: 3, nombre: 'María' }
]
*/
console.log('Usuarios actualizados:', usuariosActualizados);
/*
[
  { id: 1, nombre: 'Ana' },
  { id: 2, nombre: 'Juan Manuel' },
  { id: 3, nombre: 'María' }
]
*/
</code></pre>
<p>
    <code>with()</code> es una herramienta elegante para actualizar estados en aplicaciones que gestionan listas de ítems, simplificando lo que antes requería un patrón más verboso y propenso a errores. Para más detalles, puedes revisar la <a href="https://developer.mozilla.org/es/docs/Web/JavaScript/Reference/Global_Objects/Array/with" target="_blank" rel="noopener noreferrer">documentación de <code>Array.prototype.with</code> en MDN</a>.
</p>

<h2>Comparativa, rendimiento y cuándo usarlos</h2>

<p>
    Es natural preguntarse sobre el rendimiento cuando se habla de crear nuevas estructuras de datos en lugar de mutar las existentes. La creación de un nuevo array implica asignar nueva memoria y copiar los elementos, lo cual tiene un costo computacional. En escenarios donde se manipulan arrays extremadamente grandes en bucles muy ajustados o críticos para el rendimiento, el uso de los métodos mutables originales podría, teóricamente, ser marginalmente más rápido.
</p>
<p>
    Sin embargo, para la inmensa mayoría de las aplicaciones web y Node.js de hoy en día, el costo de rendimiento de crear un nuevo array es insignificante en comparación con los beneficios que la inmutabilidad aporta:
</p>
<ul>
    <li>
        <strong>Claridad del código:</strong> Los efectos secundarios son difíciles de rastrear. Los métodos inmutables hacen que el flujo de datos sea más predecible.
    </li>
    <li>
        <strong>Facilidad de depuración:</strong> Al no modificar el estado original, es más sencillo aislar el origen de los errores.
    </li>
    <li>
        <strong>Integración con frameworks:</strong> Frameworks como React o Vue dependen de la inmutabilidad para saber cuándo un componente debe volver a renderizarse, ya que comparan las referencias de los objetos. Si mutas un array, la referencia no cambia, y el framework podría no detectar el cambio.
    </li>
    <li>
        <strong>Concurrencia:</strong> En entornos con operaciones concurrentes o multi-hilo (como Web Workers), la inmutabilidad reduce la complejidad de la sincronización de datos y previene condiciones de carrera.
    </li>
</ul>
<p>
    Mi recomendación es adoptar los métodos inmutables como el estándar en tu código. Solo si enfrentas un cuello de botella de rendimiento demostrado y medible en una sección muy específica de tu aplicación que involucra la manipulación masiva de arrays, considera regresar a las versiones mutables, siempre con extrema precaución y documentando el porqué. Para una comprensión más profunda sobre los detalles de la propuesta y su razonamiento, puedes consultar el <a href="https://tc39.es/proposal-change-array-by-copy/" target="_blank" rel="noopener noreferrer">propuesta de TC39 sobre los métodos de array que devuelven una copia</a>.
</p>

<h2>Compatibilidad y consideraciones finales</h2>

<p>
    Los métodos <code>toReversed()</code>, <code>toSorted()</
Diario Tecnología