Immutable Array Operations In JavaScript: Introducing toSorted, toSpliced, and toReversed

Updated on · 6 min read
Immutable Array Operations In JavaScript: Introducing toSorted, toSpliced, and toReversed

Immutability in programming is a principle that enhances predictability, maintainability, and performance in applications. In JavaScript, arrays are one of the fundamental data structures, and operations on arrays are widespread across applications. Traditional array methods often modify the original array, which can lead to unexpected behaviors and bugs, especially in complex applications where data integrity is important.

Recognizing the importance of immutability, JavaScript has introduced new array methods designed to leave the original data structures untouched. These methods provide the tools to manipulate arrays while maintaining the original data state. This blog post explores the immutable array methods, such as Array.prototype.toSorted, Array.prototype.toSpliced, and Array.prototype.toReversed.

Each of these methods returns a new array, ensuring that any operation does not alter the original dataset. As developers increasingly embrace functional programming concepts, these methods align well with the principles of immutability and pure functions. They offer a path toward more robust and less error-prone code.

Immutable array operations

The recent additions to the array methods in JavaScript signal a conscious shift towards enabling and encouraging immutable operations on arrays. "Immutability" here refers to the principle whereby data is unchangeable once created, ensuring that functions do not have side effects, which might lead to mutations in the original data structure. Three notable methods that embody this principle are toSorted, toSpliced, and toReversed, introduced in ECMAScript 2023.

The distinction between these new methods and their classic counterparts lies in their approach to handling the array data. Traditional methods like Array.prototype.sort, Array.prototype.splice, and Array.prototype.reverse modify the array in place, altering the original array's content. The new immutable methods, however, generate a new array reflecting the desired changes, leaving the original array unchanged.

Why is this important? Working with immutable data:

  • Reduces complexity by eliminating side effects, making it easier to understand and predict the application's state at any given point.
  • Enhances the debugging process, as developers can track data changes more straightforwardly without unintentionally altering the original array.
  • Facilitates features in modern JavaScript frameworks, such as change detection in React, which rely on immutable data for optimizing updates to the user interface.

Let's look closer at each of these new methods to understand their significance and how they can be used in practice.

Array.prototype.toSorted

Sorting arrays and other iterable data structures in JavaScript is notorious for its potential to introduce bugs and side effects. This changes with the introduction of the toSorted method, which provides a non-destructive way to sort arrays.

The traditional Array.prototype.sort method

To understand the significance of toSorted, one must first examine the sort method. The sort method sorts the elements of an array in place and returns the sorted array. However, this in-place sort means that the original array is modified, which can lead to side effects, especially when the original order needs to be preserved for other operations or tracking purposes.

Introducing Array.prototype.toSorted

The syntax for toSorted is the same as for other array methods. The key difference lies in the fact that toSorted does not change the original array. Consider the following example:

javascript
const originalArray = [3, 1, 4, 1, 5, 9]; const sortedArray = originalArray.toSorted((a, b) => a - b); console.log(sortedArray); // [1, 1, 3, 4, 5, 9]
javascript
const originalArray = [3, 1, 4, 1, 5, 9]; const sortedArray = originalArray.toSorted((a, b) => a - b); console.log(sortedArray); // [1, 1, 3, 4, 5, 9]

In this case, sortedArray will contain [1, 1, 3, 4, 5, 9], sorted in ascending order, while originalArray will remain as [3, 1, 4, 1, 5, 9].

Benefits and use-cases

The primary benefit of using toSorted is in scenarios where the integrity of the original array must be maintained. This is especially relevant in functional programming, where functions should not have side effects. ToSorted is particularly useful in data transformation pipelines, where an array might have to be sorted in various ways as it passes through different transformations, yet the base data needs to remain untouched for other operations or logging purposes.

Furthermore, toSorted enhances code clarity by explicitly signaling that a new sorted array is being created, which can improve maintainability as other developers reading the code can immediately see that the original data is not being modified.

Array.prototype.toSpliced

The toSpliced method is a contemporary immutable alternative to the traditional splice method in JavaScript. It is used to add, remove, or replace elements in an array without altering the original array.

Limitations of the Array.prototype.splice method

The splice method alters the array on which it is called by adding, removing, or replacing elements. This mutability can cause unintentional side effects if other parts of the application rely on the original state of the array. Mutation can also lead to difficulties in tracking changes, debugging, and reasoning about the code's behavior.

For example, one common mistake in React applications is to inadvertently mutate state with splice, leading to unexpected rendering behavior.

javascript
function FruitList() { const [items, setItems] = useState(["apple", "banana", "orange"]); function removeItem(index) { // Mutates the original array items.splice(index, 1); // Attempts to update state with the mutated array won't work as expected setItems(items); // React may not re-render as the reference hasn't changed } }
javascript
function FruitList() { const [items, setItems] = useState(["apple", "banana", "orange"]); function removeItem(index) { // Mutates the original array items.splice(index, 1); // Attempts to update state with the mutated array won't work as expected setItems(items); // React may not re-render as the reference hasn't changed } }

With the new toSpliced method we can create a new array with the desired changes, leaving the original array untouched.

javascript
function FruitList() { const [items, setItems] = useState(["apple", "banana", "orange"]); function removeItem(index) { // Creates a new array with the item removed const newItems = items.toSpliced(index, 1); // Updates state with the new array setItems(newItems); // React will re-render as the reference has changed } }
javascript
function FruitList() { const [items, setItems] = useState(["apple", "banana", "orange"]); function removeItem(index) { // Creates a new array with the item removed const newItems = items.toSpliced(index, 1); // Updates state with the new array setItems(newItems); // React will re-render as the reference has changed } }

How Array.prototype.toSpliced works

Unlike splice, the toSpliced method does not modify the array it is called on. Instead, it produces a new array with the desired changes applied:

javascript
const initialArray = ["a", "b", "c", "d"]; const newArray = initialArray.toSpliced(1, 2, "x", "y"); console.log(newArray); // ['a', 'x', 'y', 'd']
javascript
const initialArray = ["a", "b", "c", "d"]; const newArray = initialArray.toSpliced(1, 2, "x", "y"); console.log(newArray); // ['a', 'x', 'y', 'd']

In this example, newArray would hold the value ['a', 'x', 'y', 'd'], which indicates that starting from index 1, two elements were removed ('b', 'c') and replaced with 'x' and 'y'. The initialArray, however, would remain untouched as ['a', 'b', 'c', 'd'].

In cases where new versions of data need to be created based upon some transformation of an existing dataset, toSpliced is incredibly useful. It shines in applications where data immutability is a feature, such as in state management libraries (e.g., Redux) and frameworks that capitalize on immutable data to optimize rendering cycles.

Array.prototype.toReversed

The toReversed method in JavaScript joins the suite of new immutable array methods, serving as a counterpart to the traditional reverse method. This method provides developers the ability to reverse the order of an array's elements without affecting the original array.

The traditional Array.prototype.reverse method

Traditionally, the reverse method would invert the order of the elements within an array in place. As a consequence, this mutates the original data structure, which can be problematic when the initial array order needs to be retained for further operations or when working within a reactive programming environment that relies on immutable data for efficient change detection.

Introducing Array.prototype.toReversed

The toReversed method exemplifies the immutable operation:

javascript
const originalNumbers = [1, 2, 3, 4, 5]; const reversedNumbers = originalNumbers.toReversed(); console.log(reversedNumbers); // [5, 4, 3, 2, 1]
javascript
const originalNumbers = [1, 2, 3, 4, 5]; const reversedNumbers = originalNumbers.toReversed(); console.log(reversedNumbers); // [5, 4, 3, 2, 1]

Following the execution of the code above, reversedNumbers will become [5, 4, 3, 2, 1], while originalNumbers will remain as [1, 2, 3, 4, 5], untouched by the reversing operation.

The toReversed method is particularly useful in scenarios where the reversed data needs to be presented or processed without altering the original dataset. This could apply in cases such as iterating through array elements in reverse for display purposes or preparing data for a backward iteration algorithm.

Conclusion

The introduction of new immutable array methods like toSorted, toSpliced, and toReversed reflects the evolving landscape of JavaScript development. Emphasizing the value of immutability, these methods offer developers strategies to manipulate arrays without compromising the integrity of the original data.

In an environment where applications are becoming increasingly complex, predictably managing state is becoming more important. Immutable operations allow for better tracking of state changes, which in turn leads to easier debugging and enhanced performance, particularly within frameworks that rely on immutable data for efficient rendering.

References and resources