JavaScript Change Array by Copy and Immutability

The Change Array by Copy specification (introduced in ECMAScript 2023) enforces functional programming and immutability patterns in JavaScript by providing built-in methods that transform arrays without modifying the original data. Traditionally, operations like sorting, reversing, or splicing arrays mutated the source array directly, forcing developers to use defensive copying techniques like spread syntax or Array.prototype.slice(). With methods such as toSorted(), toReversed(), toSpliced(), and with(), JavaScript standardizes immutable operations directly on array prototypes, reducing bugs caused by side effects and simplifying state management.

The Problem with Legacy Array Methods

Historically, core array manipulation methods in JavaScript operated via in-place mutation:

To preserve immutability in modern paradigms—such as React state management or Redux reducers—developers had to manually clone arrays before applying transformations:

// Legacy defensive copying
const sorted = [...items].sort((a, b) => a - b);
const reversed = items.slice().reverse();

While functional, this approach introduced boilerplate code, reduced readability, and left room for accidental state mutations if a clone step was omitted.

New Methods in Change Array by Copy

The Change Array by Copy specification adds four primary methods to Array.prototype and corresponding methods to TypedArray.prototype. Each method performs its transformation and returns a brand-new array instance, leaving the original array completely untouched.

1. Array.prototype.toReversed()

Returns a new array with the elements in reverse order without altering the original array.

const numbers = [1, 2, 3];
const reversed = numbers.toReversed();

console.log(reversed); // [3, 2, 1]
console.log(numbers);  // [1, 2, 3] (unchanged)

2. Array.prototype.toSorted()

Returns a new array with elements sorted according to a comparator function (or converted to strings if no comparator is provided).

const scores = [40, 100, 1, 5];
const sortedScores = scores.toSorted((a, b) => a - b);

console.log(sortedScores); // [1, 5, 40, 100]
console.log(scores);       // [40, 100, 1, 5] (unchanged)

3. Array.prototype.toSpliced()

Performs removal, insertion, or replacement operations and returns a new array, mimicking splice() without mutating the caller.

const months = ["Jan", "Mar", "Apr", "May"];
// Insert "Feb" at index 1, delete 0 elements
const updatedMonths = months.toSpliced(1, 0, "Feb");

console.log(updatedMonths); // ["Jan", "Feb", "Mar", "Apr", "May"]
console.log(months);        // ["Jan", "Mar", "Apr", "May"] (unchanged)

4. Array.prototype.with()

Allows index-based replacement by returning a copy of the array with the element at the specified index updated.

const items = ["a", "b", "c"];
const updatedItems = items.with(1, "z");

console.log(updatedItems); // ["a", "z", "c"]
console.log(items);        // ["a", "b", "c"] (unchanged)

How It Enforces Immutability Patterns

Eliminates Defensive Copying Boilerplate

Developers no longer need shallow-copy idioms ([...arr] or arr.slice()) to protect source data. The copy-on-write behavior is handled natively at the engine level, resulting in cleaner and more expressive code.

Enables Fluent Method Chaining

Because mutating methods returned the modified original array or metadata (such as the removed elements in splice()), chaining multiple transformations was either impossible or prone to unintended side effects. Copy-by-change methods return newly created arrays, enabling safe functional pipelines:

const processed = rawData
  .toSorted((a, b) => a.priority - b.priority)
  .toReversed()
  .toSpliced(0, 1);

Prevents Accidental Side Effects

Shared references across application state are a common source of bugs when functions unintentionally mutate their input parameters. By adopting non-destructive methods as the default way to transform arrays, applications guarantee that functions remain pure and data references remain stable unless explicitly reassigned.