ECMAScript proposal “Change Array by copy”: four new non-destructive Array methods

[2022-04-10] dev, javascript, es proposal
(Ad, please don’t block)

This blog post describes the ECMAScript proposal “Change Array by copy” by Robin Ricard and Ashley Claymore. It proposes four new methods for Arrays and Typed Arrays:

  • .toReversed()
  • .toSorted()
  • .toSpliced()
  • .with()

The new methods are for Arrays and TypedArrays  

This blog post only demonstrates the new methods with Arrays, but they are also available for Typed Arrays – that is, instances of the following classes:

  • Int8Array
  • Uint8Array
  • Uint8ClampedArray
  • Int16Array
  • Uint16Array
  • Int32Array
  • Uint32Array
  • Float32Array
  • Float64Array
  • BigInt64Array
  • BigUint64Array

Destructive vs. non-destructive Array methods  

Most Array methods are non-destructive – they don’t change the Arrays that they are invoked on:

// Non-destructively removing every string 'b' from `arr`
const arr = ['a', 'b', 'b', 'a'];
const result = arr.filter(x => x !== 'b');
assert.deepEqual(result, ['a', 'a']);
assert.deepEqual(arr, ['a', 'b', 'b', 'a']);

However, there are also destructive methods such as .sort() that change their receivers:

// Destructively sorting `arr`
const arr = ['c', 'a', 'b'];
const result = arr.sort();

assert.deepEqual(result, ['a', 'b', 'c']);
assert.ok(result === arr); // (A)
assert.deepEqual(arr, ['a', 'b', 'c']);

arr.sort() first sorts the Array in place and then returns it. In line A we can see that arr, the receiver of the method call, and result, the value returned by the method, are the same object.

Destructive Array methods  

These Array methods are destructive:

  • .reverse()
  • .sort()
  • .splice()

If we want to apply one of these methods to an Array without changing it, we can use one of the following patterns:

const sorted1 = arr.slice().sort();
const sorted2 = [...arr].sort();
const sorted3 = Array.from(arr).sort();

That is, we first make a copy of arr and then change that copy.

The new non-destructive methods  

The proposal introduces non-destructive versions of the three destructive Array methods so that we don’t need the aforementioned patterns anymore:

  • .toReversed(): Array

    Non-destructive version of .reverse()

  • .toSorted(compareFn): Array

    Non-destructive version of .sort()

  • .toSpliced(start, deleteCount, ...items): Array

    Non-destructive version of .splice()

It also introduces a non-destructive method that has no corresponding destructive method:

  • .with(index, value): Array

    This method non-destructively replaces an Array element at a given index (think non-destructive version of arr[index]=value).

The next sections describe these four methods in more detail.

.toReversed(): Array  

.toReversed() is the non-destructive version of .reverse():

const arr = ['a', 'b', 'c'];
assert.deepEqual(
  arr.toReversed(), ['c', 'b', 'a']
);
assert.deepEqual(
  arr, ['a', 'b', 'c']
);

This is a simple polyfill for .toReversed():

if (!Array.prototype.toReversed) {
  Array.prototype.toReversed = function () {
    return this.slice().reverse();
  };
}

.toSorted(compareFn): Array  

.toSorted() is the non-destructive version of .sort():

const arr = ['c', 'a', 'b'];
assert.deepEqual(
  arr.toSorted(), ['a', 'b', 'c']
);
assert.deepEqual(
  arr, ['c', 'a', 'b']
);

This is a simple polyfill for .toSorted():

if (!Array.prototype.toSorted) {
  Array.prototype.toSorted = function (compareFn) {
    return this.slice().sort(compareFn);
  };
}

.toSpliced(start, deleteCount, ...items): Array  

Method .splice() is more complicated than the other destructive methods:

  • It deletes deleteCount elements, starting at index start.
  • It then inserts items at index start.
  • It returns the deleted elements.

In other words – deleteCount Array elements are replaced with items:

const arr = ['a', 'b', 'c', 'd'];
// .splice() returns the deleted elements
assert.deepEqual(
  arr.splice(1, 2, 'X'), [ 'b', 'c' ]
);
// `arr` was changed
assert.deepEqual(
  arr, [ 'a', 'X', 'd' ]
);

.toSpliced() is the non-destructive version of .splice(). It needs to return the changed version of its receiver and therefore doesn’t give us access to the deleted elements:

const arr = ['a', 'b', 'c', 'd'];
assert.deepEqual(
  arr.toSpliced(1, 2, 'X'), [ 'a', 'X', 'd' ]
);
assert.deepEqual(
  arr, ['a', 'b', 'c', 'd']
);

This is a simple polyfill for .toSpliced():

if (!Array.prototype.toSpliced) {
  Array.prototype.toSpliced = function (start, deleteCount, ...items) {
    const copy = this.slice();
    copy.splice(start, deleteCount, ...items);
    return copy;
  };
}

.with(index, value): Array  

This method call:

arr.with(index, value)

is the non-destructive version of:

arr[index] = value

The following code demonstrates how .with() works:

const arr = ['a', 'b', 'c'];
assert.deepEqual(
  arr.with(1, 'X'), ['a', 'X', 'c']
);
assert.deepEqual(
  arr, ['a', 'b', 'c']
);

This is a simple polyfill for .with():

if (!Array.prototype.with) {
  Array.prototype.with = function (index, value) {
    const copy = this.slice();
    copy[index] = value;
    return copy;
  };
}

The new methods will also be available for Tuples  

The proposed ECMAScript feature Tuple is basically an immutable Array. Tuples have all methods that Arrays have – except for the destructive ones. Adding non-destructive versions of the latter to Arrays therefore helps Tuples and means that we can use the same methods to non-destructively change Arrays and Tuples.

Implementations  

The proposal lists implementations and polyfills that are currently available.

Further reading