Copying, Slicing and Flattening
Mental model: If the method name is a verb in the past tense (toSorted, toReversed), it hands you a copy. Otherwise assume it edits your array.
Level: intermediate · about 11 minutes
const a = ['a', 'b', 'c', 'd'];
console.log(a.slice(1, 3)); // ['b', 'c'], a copy
console.log(a); // ['a','b','c','d'], untouched
const b = ['a', 'b', 'c', 'd'];
console.log(b.splice(1, 3)); // ['b', 'c', 'd'], the removed part
console.log(b); // ['a'], guttedTwo methods, similar names, opposite behaviour.
slice(start, end) copies a section, with end exclusive, and never touches the original. splice(start, count) cuts a section out of the original. One letter apart in spelling, and the difference between a pure function and a mutation.
`slice()`- a shallow copy of the whole array
`slice(2)`- from index 2 to the end
`slice(1, 3)`- indexes 1 and 2, not 3
`slice(-2)`- the last two elements
`slice(0, -1)`- everything except the last
const original = [{ n: 1 }, { n: 2 }];
const shallow = [...original];
const deep = structuredClone(original);
shallow[0].n = 99;
console.log(original[0].n); // 99, same object
deep[1].n = 42;
console.log(original[1].n); // 2, untouched
Joining and flattening
console.log([1, 2].concat([3, 4], 5)); // [1,2,3,4,5] (copies, and accepts loose values)
console.log([...[1, 2], ...[3, 4]]); // [1,2,3,4], the modern spelling
console.log([1, [2, [3, [4]]]].flat()); // [1, 2, [3, [4]]] (one level only)
console.log([1, [2, [3, [4]]]].flat(2)); // [1, 2, 3, [4]]
console.log([1, [2, [3, [4]]]].flat(Infinity)); // [1, 2, 3, 4] (all the way down)
const orders = [
{ id: 1, items: ['pen', 'pad'] },
{ id: 2, items: ['ink'] },
];
console.log(orders.flatMap((o) => o.items)); // ['pen', 'pad', 'ink']
// Returning [] drops an element, returning two adds one.
console.log([1, 2, 3, 4].flatMap((n) => (n % 2 ? [n, n] : [])));
// [1, 1, 3, 3]flatMap is map then flat(1), and it is the right tool for one-to-many.
Filling in place: fill and copyWithin
console.log(new Array(4).fill(0)); // [0, 0, 0, 0]
console.log([1, 2, 3, 4].fill(9, 1, 3)); // [1, 9, 9, 4] (value, start, end)
const src = [1, 2, 3, 4, 5];
console.log(src.copyWithin(0, 3)); // [4, 5, 3, 4, 5] (target, start)
console.log(src); // same array, mutated in place
fill and copyWithin both mutate. copyWithin is rare in everyday code and common in typed-array and buffer work, where allocating a new array would defeat the point.
console.log(['a', 'b', 'c'].join('-')); // 'a-b-c'
console.log([1, 2, 3].join()); // '1,2,3', comma by default
console.log([1, null, undefined, 2].join('-')); // '1---2', empty for both
console.log([].join('-')); // '', empty string, not an errorjoin turns an array into a string. Note what happens to holes and null.
The immutable twins
ES2023 added copying versions of the four mutating methods, so you no longer need [...arr] before every edit. They exist on arrays and typed arrays, and with is the one you will reach for most: it replaces a single index and returns a new array.
const days = ['mon', 'tue', 'wed'];
console.log(days.toReversed()); // ['wed', 'tue', 'mon']
console.log(days.with(1, 'TUE')); // ['mon', 'TUE', 'wed']
console.log(days.toSpliced(1, 1)); // ['mon', 'wed']
console.log(days.toSpliced(1, 0, 'x')); // ['mon', 'x', 'tue', 'wed']
console.log(days); // ['mon','tue','wed'], all four copied
| Mutates the array | Copying alternative | Notes |
|---|---|---|
push / pop | [...arr, x] / arr.slice(0, -1) | no toPushed exists |
shift / unshift | arr.slice(1) / [x, ...arr] | spread reads better anyway |
splice | toSpliced | same arguments, returns the new array |
sort | toSorted | takes the same comparator |
reverse | toReversed | no more [...arr].reverse() |
arr[i] = v | arr.with(i, v) | accepts negative indexes |
fill, copyWithin | none | mutating by design |
| never mutates | slice, concat, map, filter, flat, flatMap, join | safe by default |
const nums = [1, 2, 3];
const copy = nums.slice();
copy.push(4);
console.log(nums.length, copy.length);slice() with no arguments returns a shallow copy, so push on the copy cannot reach the original. Had you written const copy = nums there would be one array with two names and the answer would be 4 4.
Try it yourself
Reshape without mutating
const data = ['a', 'b', 'c', 'd'];
console.log(data.slice(1, 3)); // copy of a section
console.log(data.toReversed()); // copy, reversed
console.log(data.with(0, 'A')); // copy, one index changed
console.log(data.toSpliced(1, 2)); // copy, section removed
console.log([...data, 'e']); // copy, appended
console.log('still pristine:', data);
Rewrite each mutating line as a copying one, then check that data is still in its original state at the end.
Exercises
chunk
Write chunk(arr, size) that splits an array into groups of size, with the final group holding whatever is left over. Return [] if size is less than 1, and never mutate the input.
flattenDeep
Write flattenDeep(arr) that flattens an array of arbitrarily nested arrays into a single flat array, preserving order. Do it with recursion and Array.isArray rather than flat(Infinity), so you understand what flat is doing.
Check yourself
- What does this log?
- [2, 3] 4 —
slicecopies from index 1 up to but not including index 3, and leaves the source alone. Swap insplice(1, 3)and you get[2, 3, 4]back witha.lengthreduced to 1. - Which expression returns a new array with index 2 replaced, leaving the original alone?
arr.with(2, "x")—withis the copying single-index update from ES2023, and it accepts negative indexes too. The other three all modify the original array in place.[[1, [2]], [3]].flat()gives you…- [1, [2], 3] —
flat()defaults to a depth of one, so the outer brackets go and the inner[2]survives. Passflat(Infinity)when you do not know how deep the nesting goes.
Common mistakes
- Confusing
slicewithsplice. One copies, the other cuts. - Assuming a spread or
slicecopy protects the objects inside. It does not. - Expecting
flat()to flatten all the way down. The default depth is 1.
Takeaways
slicecopies,splicemutates and returns what it removed.- All array copies are shallow.
structuredClonefor a deep one. flat(depth)defaults to 1;flatMaphandles one-to-many mapping in a single pass.toSorted,toReversed,toSplicedandwithare the copying twins worth memorising.