JavaScript: toSorted, toReversed, toSpliced e with, gli array senza mutarli

C’è un bug che prima o poi tutti scrivono in JavaScript. Una funzione riceve un array, lo ordina per mostrarlo, e da quel momento l’array è ordinato anche per chi l’aveva passato. Nessun errore, nessun avviso: solo un dato che cambia sotto i piedi di un altro pezzo di codice.
Il motivo è che sort() non restituisce una copia. MDN lo dice senza giri di parole: il metodo “sorts the elements of an array in place and returns the reference to the same array, now sorted”. Lo stesso vale per reverse() e splice().
Da ECMAScript 2023 esistono quattro metodi che fanno la stessa cosa senza toccare l’originale: toSorted(), toReversed(), toSpliced() e with(). Non sono nella specifica 2022 e compaiono in quella 2023 (sezioni 23.1.3.33–35 e 23.1.3.39). Per MDN sono “Baseline, widely available”: funzionano in tutti i browser principali da luglio 2023.
Il problema, in tre righe
L’esempio di MDN su sort() lo mostra bene:
const numbers = [3, 1, 4, 1, 5];
const sorted = numbers.sort((a, b) => a - b);
// numbers and sorted are both [1, 1, 3, 4, 5]
sorted[0] = 10;
console.log(numbers[0]); // 10
sorted e numbers non sono due array: sono lo stesso array con due nomi. Modifichi uno, cambia l’altro.
La soluzione classica era copiare prima: [...arr].sort() oppure arr.slice().sort(). Funziona, ma è un dettaglio da ricordarsi ogni volta, e la volta che te lo dimentichi nessuno se ne accorge finché un’altra parte del programma non legge l’array.
toSorted() e toReversed()
Sono le versioni “a copia” di sort() e reverse(). Stessa firma, stessa logica di confronto, ma restituiscono un array nuovo:
const values = [1, 10, 21, 2];
const sortedValues = values.toSorted((a, b) => a - b);
console.log(sortedValues); // [1, 2, 10, 21]
console.log(values); // [1, 10, 21, 2]
const items = [1, 2, 3];
const reversedItems = items.toReversed();
console.log(reversedItems); // [3, 2, 1]
console.log(items); // [1, 2, 3]
Una cosa non cambia rispetto a sort(): senza funzione di confronto gli elementi vengono convertiti in stringhe e confrontati per unità di codice UTF-16. Quindi [1, 10, 21, 2].toSorted() mette 10 prima di 2, esattamente come faceva sort(). Copiare l’array non rende l’ordinamento più intelligente.
toSpliced()
splice() fa tre cose insieme: toglie elementi, ne inserisce altri e restituisce quelli tolti. toSpliced() fa le prime due su una copia e restituisce l’array risultante:
const months = ["Jan", "Mar", "Apr", "May"];
const months2 = months.toSpliced(1, 0, "Feb");
console.log(months2); // ["Jan", "Feb", "Mar", "Apr", "May"]
const months3 = months2.toSpliced(2, 2);
console.log(months3); // ["Jan", "Feb", "May"]
console.log(months); // ["Jan", "Mar", "Apr", "May"]
Qui c’è la differenza da tenere a mente quando si sostituisce un metodo con l’altro: il valore di ritorno ha un significato diverso. Con splice() ottieni gli elementi rimossi, con toSpliced() ottieni il nuovo array. Come dice MDN, gli elementi eliminati “are not returned from this method, but they remain accessible in the original array”. Se il tuo codice usava il ritorno di splice(), non puoi cambiare solo il nome del metodo.
with()
È l’equivalente a copia di arr[i] = valore. Restituisce un array nuovo con un solo elemento cambiato, e si può concatenare:
const arr = [1, 2, 3, 4, 5];
console.log(arr.with(2, 6)); // [1, 2, 6, 4, 5]
console.log(arr.with(2, 6).map((x) => x ** 2)); // [1, 4, 36, 16, 25]
console.log(arr); // [1, 2, 3, 4, 5]
Due dettagli. L’indice può essere negativo e conta dalla fine: arr.with(-1, 0) cambia l’ultimo elemento. E a differenza dell’assegnazione con le parentesi quadre, un indice fuori dai limiti lancia un RangeError, “if index >= array.length or index < -array.length“. Con arr[10] = x l’array si allunga e si riempie di buchi; con with() ottieni un errore. È un comportamento migliore, ma è diverso: se il codice contava sull’allungamento, va riscritto.
Cosa evitare
Pensare che la copia sia profonda. Non lo è. MDN, nella sezione sui metodi a copia: “The copy always happens shallowly”. Se l’array contiene oggetti, il nuovo array contiene gli stessi oggetti:
const users = [{ name: "Anna", age: 40 }, { name: "Luca", age: 30 }];
const byAge = users.toSorted((a, b) => a.age - b.age);
byAge[0].age = 31;
console.log(users[1].age); // 31
L’ordine di users non è cambiato, ma byAge[0] e users[1] sono lo stesso oggetto. toSorted() protegge la sequenza, non il contenuto.
Aspettarsi i buchi. Gli array sparsi ([1, , 3]) escono da questi metodi senza buchi: gli slot vuoti diventano undefined. Da MDN, [1, , 3].toReversed() dà [3, undefined, 1]. Con toSorted() gli slot vuoti finiscono in fondo, come undefined. Raramente è un problema, ma se un test confronta la struttura dell’array può sorprendere.
Aspettarsi la sottoclasse. Se estendi Array con una tua classe, map() e filter() restituiscono un’istanza della sottoclasse. Questi quattro no: MDN dice che “always create new arrays with the Array base constructor”. Ti torna un Array semplice, senza i tuoi metodi.
Cercare toSpliced() sui typed array. Int32Array, Float64Array e compagni hanno toSorted(), toReversed() e with(), ma nella specifica 2023 non hanno toSpliced().
Non controllare dove gira il codice. Nei browser i metodi ci sono da luglio 2023. Se il codice gira su Node.js o su un ambiente più vecchio, verifica la versione prima di usarli: un TypeError: arr.toSorted is not a function in produzione è un modo scomodo per scoprirlo.
In breve
| Modifica l’originale | Restituisce una copia |
|---|---|
sort() |
toSorted() |
reverse() |
toReversed() |
splice() |
toSpliced() |
arr[i] = v |
arr.with(i, v) |
Non è una rivoluzione: sono quattro metodi che fanno quello che prima si faceva con una copia esplicita. Ma rendono visibile nel codice una decisione che prima era implicita. Quando leggi toSorted() sai che l’originale non si tocca, senza dover controllare se qualcuno ha messo uno spread tre righe sopra.
Se hai un’applicazione JavaScript con bug di questo tipo, o vuoi una revisione del codice esistente, scrivici.
Fonti: MDN Web Docs, pagine Array.prototype.toSorted(), toReversed(), toSpliced(), with(), sort() e la sezione “Copying methods and mutating methods” di Array; ECMAScript 2023 Language Specification.
