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()
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
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.
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 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 other destructive methods:
deleteCount
elements, starting at index start
.items
at index start
.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 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.
The proposal lists implementations and polyfills that are currently available.