Verständnis von Unveränderlichkeit in JavaScript

Avatar of Kingsley Silas
Kingsley Silas am

DigitalOcean bietet Cloud-Produkte für jede Phase Ihrer Reise. Starten Sie mit 200 $ kostenlosem Guthaben!

Wenn Sie bisher nicht mit Unveränderlichkeit in JavaScript gearbeitet haben, verwechseln Sie diese möglicherweise leicht mit dem Zuweisen einer Variablen zu einem neuen Wert oder der Neuzuweisung. Während es möglich ist, Variablen und Werte, die mit let oder var deklariert wurden, neu zuzuweisen, werden Sie auf Probleme stoßen, wenn Sie dies mit const versuchen.

Nehmen wir an, wir weisen den Wert Kingsley einer Variablen namens firstName zu

let firstName = "Kingsley";

Wir können denselben Variablen einen neuen Wert zuweisen,

firstName = "John";

Dies ist möglich, weil wir let verwendet haben. Wenn wir stattdessen const wie folgt verwenden würden

const lastName = "Silas";

…erhalten wir einen Fehler, wenn wir versuchen, ihn einem neuen Wert zuzuweisen;

lastName = "Doe"
// TypeError: Assignment to constant variable.

Das ist nicht Unveränderlichkeit.

Ein wichtiges Konzept, das Sie bei der Arbeit mit einem Framework wie React hören werden, ist, dass die Veränderung von Zuständen eine schlechte Idee ist. Das Gleiche gilt für Props. Dennoch ist es wichtig zu wissen, dass Unveränderlichkeit kein React-Konzept ist. React nutzt die Idee der Unveränderlichkeit, wenn es mit Dingen wie Zustand und Props arbeitet.

Was zum Teufel bedeutet das? Dort werden wir ansetzen.

Veränderlichkeit bedeutet, bei den Fakten zu bleiben

Unveränderliche Daten können ihre Struktur oder die darin enthaltenen Daten nicht ändern. Es handelt sich um die Festlegung eines Wertes für eine Variable, der sich nicht ändern kann, wodurch dieser Wert zu einer Tatsache oder einer Art Wahrheitsquelle wird – ähnlich wie ein Prinz einen Frosch küsst und hofft, dass er sich in einen schönen Prinzen verwandelt. Unveränderlichkeit besagt, dass dieser Frosch immer ein Frosch bleiben wird.

Objekte und Arrays hingegen erlauben Veränderung, was bedeutet, dass die Datenstruktur geändert werden kann. Das Küssen eines dieser Frösche kann tatsächlich zur Verwandlung eines Prinzen führen, wenn wir es ihm befehlen.

Nehmen wir an, wir haben ein Benutzerobjekt wie dieses

let user = { name: "James Doe", location: "Lagos" }

Als Nächstes versuchen wir, ein newUser-Objekt mit diesen Eigenschaften zu erstellen

let newUser = user

Stellen wir uns nun vor, der erste Benutzer wechselt den Standort. Dies verändert direkt das user-Objekt und beeinflusst auch das newUser

user.location = "Abia"
console.log(newUser.location) // "Abia"

Das ist vielleicht nicht das, was wir wollen. Sie sehen, wie eine solche Neuzuweisung unbeabsichtigte Folgen haben könnte.

Arbeiten mit unveränderlichen Objekten

Wir möchten sicherstellen, dass unser Objekt nicht verändert wird. Wenn wir eine Methode verwenden, muss diese ein neues Objekt zurückgeben. Im Wesentlichen benötigen wir etwas, das als reine Funktion bezeichnet wird.

Eine reine Funktion hat zwei Eigenschaften, die sie einzigartig machen

  1. Der von ihr zurückgegebene Wert hängt von der übergebenen Eingabe ab. Der zurückgegebene Wert ändert sich nicht, solange sich die Eingaben nicht ändern.
  2. Sie verändert nichts außerhalb ihres Gültigkeitsbereichs.

Durch die Verwendung von Object.assign() können wir eine Funktion erstellen, die das übergebene Objekt nicht verändert. Diese generiert stattdessen ein neues Objekt, indem sie den zweiten und dritten Parameter in das leere Objekt kopiert, das als erster Parameter übergeben wird. Dann wird das neue Objekt zurückgegeben.

const updateLocation = (data, newLocation) => {
    return {
      Object.assign({}, data, {
        location: newLocation
    })
  }
}

updateLocation() ist eine reine Funktion. Wenn wir das erste user-Objekt übergeben, gibt sie ein neues user-Objekt mit einem neuen Standortwert zurück.

Ein anderer Weg ist die Verwendung des Spread-Operators

const updateLocation = (data, newLocation) => {
  return {
    ...data,
    location: newLocation
  }
}

Okay, wie passt das alles in React? Das schauen wir uns als Nächstes an.

Unveränderlichkeit in React

In einer typischen React-Anwendung ist der Zustand ein Objekt. (Redux verwendet ein unveränderliches Objekt als Basis für den Store einer Anwendung.) Der Reconciliation-Prozess von React bestimmt, ob eine Komponente neu gerendert werden soll oder ob eine Möglichkeit benötigt wird, Änderungen zu verfolgen.

Mit anderen Worten: Wenn React nicht herausfinden kann, dass sich der Zustand einer Komponente geändert hat, weiß es nicht, dass das virtuelle DOM aktualisiert werden muss.

Unveränderlichkeit ermöglicht es, Änderungen zu verfolgen, wenn sie erzwungen wird. Dies ermöglicht es React, den alten Zustand mit seinem neuen Zustand zu vergleichen und die Komponente basierend auf diesem Unterschied neu zu rendern.

Aus diesem Grund wird die direkte Aktualisierung des Zustands in React oft abgeraten

this.state.username = "jamesdoe";

React ist sich nicht sicher, ob sich der Zustand geändert hat, und kann die Komponente nicht neu rendern.

Immutable.js

Redux hält sich an die Prinzipien der Unveränderlichkeit. Seine Reducer sind als reine Funktionen gedacht und sollten daher nicht den aktuellen Zustand verändern, sondern ein neues Objekt basierend auf dem aktuellen Zustand und der Aktion zurückgeben. Normalerweise würden wir den Spread-Operator verwenden, wie wir es zuvor getan haben, aber es ist auch möglich, dies mit einer Bibliothek namens Immutable.js zu erreichen.

Während reines JavaScript Unveränderlichkeit handhaben kann, kann es auf dem Weg zu einigen Fallstricken kommen. Die Verwendung von Immutable.js garantiert Unveränderlichkeit und bietet gleichzeitig eine reichhaltige API, die auf Leistung ausgelegt ist. Wir werden in diesem Beitrag nicht auf alle Details von Immutability.js eingehen, aber wir werden ein kurzes Beispiel betrachten, das die Verwendung in einer To-Do-Anwendung zeigt, die von React und Redux angetrieben wird.

Zuerst importieren wir die benötigten Module und richten die Todo-Komponente ein.


const { List, Map } = Immutable;
const { Provider, connect } = ReactRedux;
const { createStore } = Redux;

Wenn Sie lokal mitarbeiten, müssen Sie diese Pakete installiert haben

npm install redux react-redux immutable 

Die Importanweisungen sehen wie folgt aus.

import { List, Map } from "immutable";
import { Provider, connect } from "react-redux";
import { createStore } from "redux";

Dann können wir unsere Todo-Komponente mit etwas Markup einrichten

const Todo = ({ todos, handleNewTodo }) => {
  const handleSubmit = event => {
    const text = event.target.value;
    if (event.keyCode === 13 && text.length > 0) {
      handleNewTodo(text);
      event.target.value = "";
    }
  };

  return (
    <section className="section">
      <div className="box field">
        <label className="label">Todo</label>
        <div className="control">
          <input
            type="text"
            className="input"
            placeholder="Add todo"
            onKeyDown={handleSubmit}
          />
        </div>
      </div>
      <ul>
        {todos.map(item => (
          <div key={item.get("id")} className="box">
            {item.get("text")}
          </div>
        ))}
      </ul>
    </section>
  );
};

Wir verwenden die Methode handleSubmit(), um neue To-Do-Elemente zu erstellen. Für dieses Beispiel wird der Benutzer nur neue To-Do-Elemente erstellen und wir benötigen dafür nur eine Aktion

const actions = {
  handleNewTodo(text) {
    return {
      type: "ADD_TODO",
      payload: {
        id: uuid.v4(),
        text
      }
    };
  }
};

Die von uns erstellte payload enthält die ID und den Text des To-Do-Elements. Dann können wir unsere Reducer-Funktion einrichten und die obige Aktion an die Reducer-Funktion übergeben

const reducer = function(state = List(), action) {
  switch (action.type) {
    case "ADD_TODO":
      return state.push(Map(action.payload));
    default:
      return state;
  }
};

Wir werden connect verwenden, um eine Container-Komponente zu erstellen, damit wir uns mit dem Store verbinden können. Dann müssen wir mapStateToProps() und mapDispatchToProps() Funktionen an connect übergeben.

const mapStateToProps = state => {
  return {
    todos: state
  };
};

const mapDispatchToProps = dispatch => {
  return {
    handleNewTodo: text => dispatch(actions.handleNewTodo(text))
  };
};

const store = createStore(reducer);

const App = connect(
  mapStateToProps,
  mapDispatchToProps
)(Todo);

const rootElement = document.getElementById("root");

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  rootElement
);

Wir verwenden mapStateToProps(), um der Komponente die Daten des Stores zuzuführen. Dann verwenden wir mapDispatchToProps(), um die Action Creators als Props für die Komponente verfügbar zu machen, indem wir die Aktion daran binden.

In der Reducer-Funktion verwenden wir List von Immutable.js, um den Anfangszustand der App zu erstellen.

const reducer = function(state = List(), action) {
  switch (action.type) {
    case "ADD_TODO":
      return state.push(Map(action.payload));
    default:
      return state;
  }
};

Stellen Sie sich List wie ein JavaScript-Array vor, weshalb wir die .push()-Methode auf dem Zustand verwenden können. Der Wert, der zur Aktualisierung des Zustands verwendet wird, ist ein Objekt, das angibt, dass Map als Objekt erkannt werden kann. Auf diese Weise ist es nicht notwendig, Object.assign() oder den Spread-Operator zu verwenden, da dies garantiert, dass der aktuelle Zustand nicht geändert werden kann. Dies sieht viel sauberer aus, besonders wenn sich herausstellt, dass der Zustand tief verschachtelt ist – wir brauchen keine Spread-Operatoren überall.


Unveränderliche Zustände ermöglichen es dem Code, schnell festzustellen, ob eine Änderung eingetreten ist. Wir müssen keine rekursive Vergleiche der Daten durchführen, um festzustellen, ob eine Änderung stattgefunden hat. Dennoch ist es wichtig zu erwähnen, dass bei der Arbeit mit großen Datenstrukturen Leistungsprobleme auftreten können – das Kopieren großer Datenobjekte hat seinen Preis.

Aber Daten müssen sich ändern, denn sonst gäbe es keinen Sinn für dynamische Websites oder Anwendungen. Wichtig ist, wie die Daten geändert werden. Unveränderlichkeit bietet die richtige Methode, um die Daten (oder den Zustand) einer Anwendung zu ändern. Dies ermöglicht es, die Änderungen des Zustands zu verfolgen und zu bestimmen, welche Teile der Anwendung als Ergebnis dieser Änderung neu gerendert werden sollen.

Das Erlernen der Unveränderlichkeit wird beim ersten Mal verwirrend sein. Aber Sie werden besser, wenn Sie auf Fehler stoßen, die auftreten, wenn der Zustand verändert wird. Das ist oft der klarste Weg, um die Notwendigkeit und die Vorteile der Unveränderlichkeit zu verstehen.

Weitere Lektüre