Immutable Arrays für JavaScript

the next big thing  –  4 Kommentare

Unveränderliche Datentypen sind in JavaScript spätestens seit dem Erfolg von React ein wichtiges Thema. Häufig wird für die Umsetzung auf Module wie Immutable.js verwiesen, doch mit erstaunlich wenig Aufwand lassen sich die Funktionen auch selbst schreiben.

JavaScript unterscheidet, wie viele andere Sprachen auch, Werte- und Referenztypen. Eine Variable, die einen Wertetyp repräsentiert, lässt sich ändern. Dabei wird der aktuelle Wert durch den neuen ersetzt. Das betrifft aber nur die Variable an sich, keine Kopien des Werts, wie das folgende Beispiel zeigt:

let foo = 23,
bar = foo;

foo = 42;

console.log(foo, bar); // => 42, 23

Referenztypen verhalten sich im Prinzip genauso. Das gilt allerdings nur dann, wenn die Referenz an sich geändert wird:

let foo = { text: 'now' },
bar = foo;

foo = { text: 'then' };

console.log(foo, bar); // => { text: 'then' }, { text: 'now' }

Wird hingegen eine Eigenschaft des Objekts geändert, ändert sich dessen Referenz nicht. Da beide Variablen, foo und bar, nach wie vor auf die Referenz darstellen und auf dasselbe Objekt verweisen, hat sich der Wert von beiden verändert:

let foo = { text: 'now' },
bar = foo;

foo.text = 'then';

console.log(foo, bar); // => { text: 'then' }, { text: 'then' }

Das alles ist zunächst wenig verwunderlich und gehört zu den Grundlagen der Programmierung. Trotzdem wird es in der Praxis allzu gerne vergessen, wie man gelegentlich in Code sehen kann, der für die UI-Bibliothek React geschrieben wurde.

Der in React-Komponenten verwendete State ist ein Objekt. Da es sich also um einen Referenztyp handelt und React die Identität von Referenzen als Grundlage dafür verwendet, Änderungen erkennen zu können, darf der State nicht direkt verändert werden. Die Zeile

this.state.text = 'then';

ist in React daher falsch. Stattdessen ist die Funktion setState zu verwenden, die sich darum kümmert, ein neues State-Objekt zu erzeugen und intern zuzuweisen:

this.setState({
text: 'then'
});

Beschäftigt man sich mit React, begegnet man der setState-Funktion in der Regel frühzeitig. Das Problem ist also hinlänglich bekannt – glaubt man. In der Praxis ist nämlich häufig Code zu sehen, der sinngemäß dem folgenden Beispiel entspricht:

const texts = this.state.texts;

texts.push('then');

this.setState({
texts
});

Was dabei vergessen wird, ist, dass Arrays ihrerseits Referenztypen sind, die initiale Zuweisung also lediglich die Referenz kopiert. Ruft man anschließend die push-Funktion auf, ändert das nicht nur die vermeintlich gerade erzeugte lokale Kopie, sondern auch den State an sich. Dass danach noch einmal setState aufgerufen wird, behebt das Problem nur bedingt.

Um das Problem zu lösen, müsste man zunächst eine echte Kopie des Arrays erstellen und erst dann die push-Funktion aufrufen. Idealerweise wäre die push-Funktion von JavaScript bereits so implementiert, dass sie eine neue Kopie des Arrays zurückgibt. Leider ist das nicht der Fall.

Als Ausweg werden häufig Bibliotheken wie Immutable.js verwendet. Erzeugt man beispielsweise eine Liste und ruft auf dieser push auf, erhält man tatsächlich eine neue Kopie. Die ursprüngliche Liste bleibt unangetastet. Prinzipiell ist das also eine gute Lösung, gelegentlich schießt man damit jedoch ein wenig über das Ziel hinaus.

Daher wäre es schön, die typischen Array-Funktionen als leichtgewichtige Alternative zur Verfügung zu haben, ohne dafür ein verhältnismäßig großes Modul einbinden zu müssen. Glücklicherweise lassen sich die Funktionen leicht selbst schreiben.

Als Basis für viele dieser Funktionen dient der -Operator, der ein gegebenes Array in eine kommaseparierte Liste von Werten umwandelt. Das ist beispielsweise dann praktisch, wenn man die Werte eines Array als Parameter an eine Funktion übergeben möchte, diese die Werte aber einzeln erwartet:

const add = function (left, right) {
return left + right;
};

const numbers = [ 23, 42 ];

const sum = add(...numbers);

console.log(sum); // => 65

Umschließt man ein derart auseinandergebautes Array wiederum mit einem Paar eckiger Klammern, erzeugt man aus den Werten ein neues Array:

const primes = [ 2, 3, 5, 7, 11 ];
const copy = [ ...primes ];

console.log(primes === copy); // => false

Auf dem Weg lässt sich beispielsweise eine push-Funktion schreiben, die das veränderte Array als Kopie zurückgibt und das ursprüngliche unverändert erhält:

const push = function (array, value) {
return [ ...array, value ];
};

Das Gleiche gilt für die unshift-Funktion. Auch pop und shift lassen sich ähnlich einfach implementieren. In dem Fall kommt allerdings die slice-Funktion zum Einsatz, die von Haus aus bereits eine Kopie zurückgibt:

const pop = function (array) {
return array.slice(0, -1);
};

Auf dem gleichen Weg lassen sich noch weitere Funktionen implementieren, beispielsweise reverse. Da die Funktion das Array verändert, liegt es nahe, das Array zunächst zu kopieren und erst danach umzukehren:

const reverse = function (array) {
return [ ...array ].reverse();
};

Greift man das zuvor genannte Beispiel aus React auf, lässt sich der Code nun folgendermaßen umschreiben:

const texts = this.state.texts,
newTexts = [ ...texts, 'then' ];

this.setState({
texts: newTexts
});

Der Code ist nicht nur klarer als die vorige Version, er ist zudem auch kürzer und vor allem korrekt: Das ursprüngliche Array bleibt bei dem Vorgehen unangetastet.

tl;dr: Unveränderliche Arrays lassen sich in JavaScript leicht selbst implementieren, vor allem Dank des …-Operators. In React kann das helfen, besseren Code zu schreiben, ohne unbedingt ein relativ umfangreiches Modul einbinden zu müssen.