Overview
Immutability is the state of an object or value that cannot be modified after it is created. Instead of changing the existing data, any operation that would modify the value must create a new copy of the data with the changes applied.
Core Concepts
- Mutable vs. Immutable:
- Mutable: The object can be changed in place (e.g., adding an item to a JavaScript array).
- Immutable: The object cannot be changed. To “update” it, you create a new object (e.g., adding a string to another string in Python or Java).
- Deep vs. Shallow Immutability:
- Shallow Immutability: Only the top-level properties are protected. If a property is another object, that nested object can still be modified.
- Deep Immutability: Every level of the data structure is recursively immutable.
- Persistent Data Structures: Specialized data structures (like those found in Clojure or Immutable.js) that use structural sharing to make creating copies efficient, avoiding the need to copy the entire object every time.
- Copy-on-Write: A strategy where a resource is shared until a modification is attempted, at which point a copy is made for the modifying process.
Code Examples
// Mutable (The original array is changed)
const mutableArray = [1, 2, 3];
mutableArray.push(4);
console.log(mutableArray); // [1, 2, 3, 4]
// Immutable (The original array remains unchanged)
const immutableArray = [1, 2, 3];
const newArray = [...immutableArray, 4]; // Spread operator creates a new copy
console.log(immutableArray); // [1, 2, 3]
console.log(newArray); // [1, 2, 3, 4]
// Deep Immutability (using Object.freeze in JS)
const user = { name: "Alice", settings: { theme: "dark" } };
Object.freeze(user);
user.name = "Bob"; // Fails silently (or throws error in strict mode)
user.settings.theme = "light"; // Still works! (Shallow freeze)
Use Cases
- State Management: In frameworks like React or Redux, immutability is used to detect state changes efficiently. If the reference to the state object changes, the UI knows it needs to re-render.
- Concurrency: Immutable objects are inherently thread-safe. Since they cannot change, multiple threads can read the same object without needing locks or mutexes to prevent race conditions.
- Undo/Redo Functionality: By keeping a history of immutable states, implementing “undo” is as simple as reverting to a previous version of the state object.
- Functional Programming: Immutability is a core pillar of functional programming, emphasizing pure functions that return new values rather than mutating inputs.
Gotchas
- Performance Overhead: Creating new copies of large objects can lead to increased memory usage and garbage collection pressure. This is where structural sharing becomes essential.
- Boilerplate: Manually copying nested objects (e.g.,
{...state, user: {...state.user, name: 'Bob'}}) can become verbose and error-prone.
