Immutable Array-Methoden: with(), toSorted(), toReversed(), toSpliced()
AI generated
JS
() =>
JavaScript · Sprachfeatures · State Management
Arrays aendern, ohne sie zu mutieren:
with(), toSorted(), toReversed(), toSpliced()

Vier neue Array-Methoden liefern eine veraenderte Kopie statt das Original zu mutieren. Das macht sort(), reverse() und splice() in State-getriebenem Code endlich ueberfluessig.

16 Min. Lesezeit ES2023 Immutability React & Redux

1. Warum mutierende Array-Methoden ein Problem sind

JavaScript-Arrays kennen seit jeher zwei Kategorien von Methoden: solche, die eine neue Kopie zurueckgeben (map(), filter(), slice()), und solche, die das Array an Ort und Stelle veraendern (sort(), reverse(), splice()). Letztere aendern nicht nur den Inhalt des Arrays, sie geben oft sogar dasselbe Array-Objekt als Rueckgabewert zurueck, was den Fehler kaschiert. Wer const sorted = liste.sort() schreibt, hat kein neues Array erzeugt, sondern liste selbst permanent veraendert und lediglich eine zweite Referenz darauf angelegt.

In simplen Skripten faellt das selten auf. Sobald ein Array aber an mehreren Stellen referenziert wird, etwa als Teil eines React-States, eines Redux-Stores oder als Funktionsparameter, der eigentlich nur gelesen werden sollte, wird die Mutation zur Fehlerquelle. Ein Component-State, der sich unter der Hand aendert, ohne dass ein Re-Render ausgeloest wird, ist einer der klassischsten React-Bugs ueberhaupt. Die neuen immutablen Geschwistermethoden loesen dieses Problem an der Wurzel, indem sie exakt dieselbe Funktionalitaet bieten, aber grundsaetzlich eine neue Kopie zurueckgeben.

2. with(): ein einzelnes Element ersetzen

Array.prototype.with(index, value) ersetzt das Element an Position index und gibt ein komplett neues Array zurueck, das Original bleibt unangetastet. Vorher war dafuer meist ein Umweg noetig: entweder array.slice() gefolgt von direkter Indexzuweisung, oder eine Kombination aus Spread-Operator und Template-Trickserei. Beides ist umstaendlich und fehleranfaellig, insbesondere bei negativen Indizes, die with() uebrigens ebenfalls unterstuetzt, genau wie at().

Der praktische Nutzen zeigt sich sofort in typischen UI-Szenarien: eine Todo-Liste, bei der ein einzelner Eintrag als erledigt markiert wird, oder ein Formular-Array, bei dem ein Feld korrigiert werden muss. Statt das ganze Array manuell zu kopieren und den Index von Hand zu setzen, reicht ein einziger, lesbarer Aufruf. Das reduziert nicht nur Codezeilen, sondern auch die Wahrscheinlichkeit, versehentlich doch das Original zu mutieren.


const todos = ["Einkaufen", "Waesche", "Putzen"];

// Alt: manuelle Kopie noetig
const updatedOld = [...todos];
updatedOld[1] = "Waesche (erledigt)";

// Neu: with() liefert direkt eine neue, veraenderte Kopie
const updated = todos.with(1, "Waesche (erledigt)");

console.log(todos);   // ["Einkaufen", "Waesche", "Putzen"] -- unveraendert
console.log(updated); // ["Einkaufen", "Waesche (erledigt)", "Putzen"]

// Funktioniert auch mit negativen Indizes wie at()
const lastReplaced = todos.with(-1, "Kochen");
console.log(lastReplaced); // ["Einkaufen", "Waesche", "Kochen"]

3. toSorted() und toReversed(): sortieren ohne Seiteneffekte

toSorted() und toReversed() verhalten sich exakt wie ihre mutierenden Vorbilder sort() und reverse(), inklusive identischer Comparator-Funktion bei toSorted(), geben aber jeweils eine neue Array-Instanz zurueck. Das Original bleibt in seiner urspruenglichen Reihenfolge erhalten. Wer schon einmal den Bug erlebt hat, dass eine an mehreren Stellen im Code gemeinsam genutzte Liste ploetzlich in falscher Reihenfolge angezeigt wurde, weil irgendwo sort() ohne Kopie aufgerufen wurde, weiss, wie subtil dieser Fehler sein kann.

Besonders in Komponenten, die Daten nur zur Anzeige sortieren, ohne die zugrunde liegende Datenquelle zu veraendern, ist toSorted() die richtige Wahl. Ein Sortier-Dropdown in einer Produktliste etwa soll die Ansicht umsortieren, nicht aber die Reihenfolge im eigentlichen Datenmodell durcheinanderbringen, das vielleicht an anderer Stelle noch die urspruengliche Reihenfolge braucht, etwa fuer einen Undo-Mechanismus oder eine zweite Ansicht.


const preise = [42, 7, 19, 3, 88];

const sortiert = preise.toSorted((a, b) => a - b);
const umgekehrt = preise.toReversed();

console.log(preise);    // [42, 7, 19, 3, 88] -- unveraendert
console.log(sortiert);  // [3, 7, 19, 42, 88]
console.log(umgekehrt); // [88, 3, 19, 7, 42]

// Praktisch in einer React-Reducer-Funktion:
function sortReducer(state, action) {
  switch (action.type) {
    case "SORT_ASC":
      return { ...state, items: state.items.toSorted((a, b) => a.value - b.value) };
    default:
      return state;
  }
}

4. toSpliced(): einfuegen und entfernen ohne Mutation

splice() ist wohl die vielseitigste, aber auch gefaehrlichste Array-Methode: sie kann gleichzeitig Elemente entfernen, einfuegen und ersetzen, mutiert dabei aber immer das Array und gibt zur Verwirrung noch die entfernten Elemente zurueck statt des veraenderten Arrays. toSpliced() uebernimmt exakt dieselbe Signatur, Start-Index, Anzahl zu loeschender Elemente, einzufuegende Elemente, gibt aber die vollstaendige neue Array-Kopie zurueck und laesst das Original in Ruhe.

Damit lassen sich komplexe Listenoperationen wie das Einfuegen eines neuen Eintrags an bestimmter Position oder das Entfernen eines Bereichs in einer einzigen, deklarativen Zeile ausdruecken, ganz ohne Spread-Gymnastik mit slice(0, i) und slice(i + 1). Gerade in Redux-Reducern oder Zustand-Stores, wo jede Mutation des States gegen die Grundregeln verstoesst, ist toSpliced() ein direkter Ersatz fuer fehleranfaellige manuelle Spread-Kombinationen.


const warenkorb = ["Apfel", "Birne", "Kirsche", "Traube"];

// Ein Element an Position 2 einfuegen, nichts loeschen
const eingefuegt = warenkorb.toSpliced(2, 0, "Pflaume");
console.log(eingefuegt); // ["Apfel", "Birne", "Pflaume", "Kirsche", "Traube"]

// Zwei Elemente ab Position 1 entfernen
const entfernt = warenkorb.toSpliced(1, 2);
console.log(entfernt); // ["Apfel", "Traube"]

console.log(warenkorb); // ["Apfel", "Birne", "Kirsche", "Traube"] -- unveraendert

5. Praktischer Nutzen in React- und Redux-artigem State

React und Redux basieren auf dem Prinzip, dass State-Aenderungen immer neue Objektreferenzen erzeugen muessen, damit Change-Detection und Re-Renders zuverlaessig funktionieren. Vor den neuen Methoden war der uebliche Workaround, mutierende Array-Methoden auf einer zuvor per Spread erzeugten Kopie aufzurufen: [...state.items].sort(...). Das funktioniert, ist aber ein zusaetzlicher gedanklicher Schritt, den man bei jedem einzelnen Update erneut korrekt ausfuehren muss, und ein vergessenes Spread ist ein klassischer, schwer zu findender Bug.

Mit toSorted(), toReversed(), with() und toSpliced() entfaellt dieser Zwischenschritt vollstaendig, weil die Immutability direkt in der Methode eingebaut ist. Das reduziert nicht nur Tippfehlerquellen, sondern macht Reducer-Funktionen auch besser lesbar, weil die Absicht direkt aus dem Methodennamen hervorgeht: Ein toSorted()-Aufruf signalisiert sofort, dass hier bewusst eine sortierte Kopie erzeugt wird, ohne dass man erst pruefen muss, ob ein Spread davor steht.

6. Strukturelles Teilen und Performance-Ueberlegungen

Ein haeufiges Missverstaendnis ist, dass immutable Operationen automatisch teuer seien, weil ja immer eine Kopie erzeugt wird. Bei den neuen Array-Methoden stimmt das nur teilweise: Es wird tatsaechlich ein neues Array mit neuen internen Referenzen angelegt, die enthaltenen Objekte selbst werden aber nicht tief kopiert, sondern nur ihre Referenzen uebernommen. Ein Array aus tausend Objekten zu sortieren kostet also nicht das Kopieren von tausend Objekten, sondern lediglich das Anlegen eines neuen Arrays mit denselben tausend Referenzen in neuer Reihenfolge.

Fuer sehr grosse Arrays, die bei jedem einzelnen State-Update komplett neu angelegt werden, kann die Kopie dennoch messbar ins Gewicht fallen, insbesondere bei Listen mit zehntausenden Eintraegen, die mehrfach pro Sekunde aktualisiert werden. In solchen Faellen lohnt sich der Blick auf spezialisierte Persistent-Data-Structure-Bibliotheken mit strukturellem Teilen auf Baumebene. Fuer die allermeisten UI-Anwendungsfaelle, Formulare, Listen mit wenigen hundert Eintraegen, Sortier-Dropdowns, ist der Overhead der neuen Methoden jedoch vernachlaessigbar und der Lesbarkeitsgewinn ueberwiegt deutlich.

7. Typische Fehlerquellen und Migrationsstrategie

Ein subtiler Stolperstein: toSorted() mit Comparator-Funktion sortiert exakt wie sort(), das heisst Zahlen werden ohne Comparator standardmaessig lexikografisch als Strings verglichen. [10, 2, 33].toSorted() liefert also [10, 2, 33] nicht numerisch aufsteigend, sondern lexikografisch, was bei numerischen Arrays oft nicht das gewuenschte Ergebnis ist. Der Comparator (a, b) => a - b bleibt daher weiterhin notwendig, die neuen Methoden aendern nur das Mutationsverhalten, nicht die Sortierlogik selbst.

Fuer die Migration bestehender Codebasen empfiehlt sich ein schrittweises Vorgehen: Zuerst alle Stellen identifizieren, an denen sort(), reverse() oder splice() auf State-Objekten aufgerufen werden, typischerweise per Codesuche oder ESLint-Regel wie no-param-reassign in Kombination mit einer eigenen Regel gegen mutierende Methoden auf Props oder State. Anschliessend koennen diese Stellen direkt durch die immutablen Gegenstuecke ersetzt werden, ohne dass sich die uebrige Logik aendern muss, da Parameter und Rueckgabewert bis auf die Mutation identisch funktionieren.

8. Browser-Unterstuetzung und Polyfill-Strategie

Die vier Methoden sind Teil des ES2023-Standards und seit Ende 2023 in allen aktuellen Versionen von Chrome, Firefox, Safari und Node.js ab Version 20 verfuegbar. Fuer Projekte, die aeltere Browser oder LTS-Node-Versionen unterstuetzen muessen, gibt es das core-js-Polyfill-Paket, das die Methoden transparent nachruestet, ohne dass Anwendungscode angepasst werden muss. Ein Feature-Check per Array.prototype.toSorted === undefined vor dem Laden des Polyfills spart in modernen Umgebungen unnoetigen Overhead.

Fuer TypeScript-Projekte ist zusaetzlich ein aktuelles lib-Target in der tsconfig.json erforderlich, mindestens ES2023, damit der Compiler die neuen Methoden ohne Typfehler kennt. Wer TypeScript mit einem aelteren Target wie ES2020 kompiliert, aber die Runtime trotzdem ES2023-faehig ist, kann die lib-Option unabhaengig vom Kompilierungsziel setzen und so die neuen Typdefinitionen nutzen, ohne die Ausgabesyntax zu veraendern.

9. Vergleichstabelle: mutierend vs. immutabel

Die folgende Tabelle stellt jede mutierende Methode ihrem immutablen Gegenstueck direkt gegenueber und zeigt, welcher Rueckgabewert jeweils zu erwarten ist. Wichtig dabei: Die Parameter sind bei jedem Paar identisch, es aendert sich ausschliesslich das Mutationsverhalten und der Rueckgabewert, sodass eine Migration in der Regel ein reines Suchen-und-Ersetzen des Methodennamens ist.

Als Faustregel gilt: Immer dann, wenn ein Array-Ergebnis in einem State, einem Redux-Store, einem React-Context oder als Rueckgabewert einer reinen Funktion landet, sind die immutablen Varianten die sicherere Wahl. Nur in performancekritischen, rein lokalen Schleifen ohne geteilte Referenzen ist die mutierende Variante weiterhin gerechtfertigt, weil sie eine Array-Allokation einspart.

Mutierende Methode Immutable Methode Rueckgabewert Original
sort() toSorted() Neues sortiertes Array Unveraendert
reverse() toReversed() Neues umgekehrtes Array Unveraendert
splice() toSpliced() Neues Array mit Aenderung Unveraendert
Index-Zuweisung arr[i] = with(i, value) Neues Array mit ersetztem Element Unveraendert

Mironsoft

Moderne Browser-APIs, Performance und wartbares JavaScript

JavaScript, das im echten Browser robust bleibt, nicht nur im Tutorial?

Wir prüfen bestehenden Frontend-Code auf veraltete Patterns, unnötige Bibliotheken und Performance-Fallen und ersetzen sie durch moderne, native Browser-APIs, die weniger Bundle-Gewicht und weniger Wartungslast bedeuten.

Code-Review

Veraltete Patterns, unnötige Dependencies und Memory Leaks systematisch aufspüren.

Performance-Optimierung

Bundle-Größe, Ladezeit und Runtime-Performance mit modernen APIs verbessern.

Modernisierung

Native Browser-APIs statt schwerer Bibliotheken gezielt einführen.

10. Zusammenfassung

Immutable Array-Methoden auf einen Blick

with()

Ersetzt ein Element per Index, liefert eine neue Array-Kopie, unterstuetzt negative Indizes wie at().

toSorted() / toReversed()

Sortieren bzw. umkehren ohne das Original zu veraendern, identische Comparator-Signatur wie sort().

toSpliced()

Einfuegen, entfernen und ersetzen in einem Aufruf, liefert das neue Array statt der entfernten Elemente.

Einsatzgebiet

React-State, Redux-Reducer und jede Stelle, an der geteilte Array-Referenzen vor versehentlicher Mutation geschuetzt werden muessen.

11. FAQ: Immutable Array-Methoden auf einen Blick

1Was ist der Hauptunterschied zwischen sort() und toSorted()?
sort() sortiert das Array direkt an Ort und Stelle und gibt dieselbe Referenz zurueck, toSorted() laesst das Original unangetastet und liefert eine komplett neue Array-Instanz mit derselben Sortierlogik.
2Kann ich bei toSorted() weiterhin eine eigene Comparator-Funktion verwenden?
Ja, toSorted() akzeptiert exakt dieselbe optionale Comparator-Funktion wie sort(). Ohne Comparator wird weiterhin lexikografisch als String sortiert, auch bei Zahlen-Arrays.
3Werden bei with() und toSpliced() auch die enthaltenen Objekte kopiert?
Nein, es findet nur ein flaches Kopieren statt. Die neue Array-Struktur enthaelt dieselben Objekt-Referenzen wie das Original, lediglich die Array-Huelle selbst ist neu. Aenderungen an den enthaltenen Objekten wirken sich also weiterhin auf beide Arrays aus.
4Ab welcher Node.js-Version sind die Methoden verfuegbar?
Ab Node.js 20 sind with(), toSorted(), toReversed() und toSpliced() nativ ohne Flags verfuegbar. Fuer aeltere Node-LTS-Versionen wird ein Polyfill wie core-js benoetigt.
5Muss ich alte sort()-Aufrufe im Code jetzt zwingend ersetzen?
Nicht zwingend. In rein lokalen, kurzlebigen Variablen ohne geteilte Referenzen ist sort() weiterhin voellig in Ordnung. Kritisch wird es nur, wenn das Array Teil eines geteilten States oder einer Prop ist.
6Was passiert bei with(), wenn der Index ausserhalb des Arrays liegt?
with() wirft einen RangeError, wenn der Index ausserhalb des gueltigen Bereichs liegt. Anders als eine direkte Indexzuweisung erlaubt with() kein automatisches Vergroessern des Arrays ueber das Ende hinaus.
7Sind die neuen Methoden performanter als die alte Spread-plus-Mutation-Kombination?
In der Regel vergleichbar, teils minimal schneller, da die Engine die Kopieroperation intern optimieren kann. Der eigentliche Vorteil liegt aber in der Lesbarkeit und der geringeren Fehleranfaelligkeit, nicht primaer in der reinen Geschwindigkeit.
8Funktionieren toSorted() und toReversed() auch auf TypedArrays?
Ja, TypedArrays wie Int32Array oder Float64Array besitzen ebenfalls with(), toSorted(), toReversed() und eine Variante namens toSpliced() existiert dort allerdings nicht, da TypedArrays eine feste Laenge haben.
9Kann ich toSpliced() nutzen, um mehrere Elemente gleichzeitig einzufuegen?
Ja, alle Argumente ab dem dritten werden als einzufuegende Elemente behandelt, genau wie bei splice(). arr.toSpliced(1, 0, 'a', 'b', 'c') fuegt drei Elemente an Position 1 ein.
10Ersetzen die neuen Methoden Immutable.js oder Immer in React-Projekten?
Fuer einfache Array-Operationen machen sie spezialisierte Bibliotheken oft ueberfluessig. Bei tief verschachtelten Objektstrukturen bieten Immer und Immutable.js aber weiterhin Mehrwert durch strukturelles Teilen ueber Objektebenen hinweg, nicht nur auf Array-Ebene.