Chunk an Array
Split an array into pieces of size n. Remainder chunk, size <= 0, and why splice in a loop mutates the input you were asked not to touch.
- javascript
- arrays
- interview
Pagination, batch uploads, and “render 50 rows at a time” all need the same helper: take [1, 2, 3, 4, 5] and 2, return [[1, 2], [3, 4], [5]]. The last piece is shorter. That is not a special case. That is what remainder means.
The usual own-goal is while (arr.length) out.push(arr.splice(0, n)). It works, it is short, and it empties the input. The caller still holds arr. Tests that reuse the fixture fail. React state that pointed at the list is now []. Chunk an array is a pure function unless the prompt says mutate.
- Walk with a stride of
n.slice(i, i + n)copies;splicemutates.- The last chunk is shorter when
length % n !== 0. Don’t pad.n <= 0or a non-integernis aRangeError(or you infinite-loop).- Empty input returns
[], not[[]].- Holes in the source stay holes in the slices. Don’t invent
undefinedfills unless asked.
Slice, do not splice, unless the prompt says mutate
Array.prototype.slice(start, end) returns a shallow copy of [start, end). It does not touch the original. A loop that adds n to i is the whole implementation.
function chunk(arr, size) {
if (!Number.isInteger(size) || size <= 0) {
throw new RangeError('size must be a positive integer');
}
const out = [];
for (let i = 0; i < arr.length; i += size) {
out.push(arr.slice(i, i + size));
}
return out;
}
chunk([1, 2, 3, 4, 5], 2); // [[1, 2], [3, 4], [5]]
chunk([1, 2, 3], 3); // [[1, 2, 3]]
chunk([1, 2, 3], 5); // [[1, 2, 3]]
chunk([], 2); // []
Shallow copy: each chunk holds the same object references as the source. chunk([{ a: 1 }], 1)[0][0] === source[0]. Mutating a nested object still aliases. Chunk is not structured clone.
Procedure
- Reject
sizethat is not an integer>= 1. - If
arr.length === 0, return a new empty array. - Set
i = 0. Whilei < arr.length, pusharr.slice(i, i + size), theni += size. - Return
out. Never assign intoarr.
arr |
size |
Result |
|---|---|---|
[1,2,3,4,5] |
2 |
[[1,2],[3,4],[5]] |
[1,2,3] |
1 |
[[1],[2],[3]] |
[1,2,3] |
10 |
[[1,2,3]] |
[] |
3 |
[] |
[1,2] |
0 |
throw |
Why splice is a trap
function chunkDestructive(arr, size) {
const out = [];
while (arr.length) {
out.push(arr.splice(0, size));
}
return out;
}
const source = [1, 2, 3, 4];
chunkDestructive(source, 2);
source; // []
splice also returns the removed piece, so the code looks clean. It is the wrong primitive for a helper named like _.chunk. If you must use splice, splice a copy: const copy = arr.slice(); while (copy.length) .... You still pay O(n) shifts on every splice(0, size) from the front. The index-and-slice loop is O(n) copies of references and does not reshuffle the tail.
Array.from({ length: Math.ceil(arr.length / size) }, (_, i) => arr.slice(i * size, i * size + size)) is the same algorithm in one expression. Fine if you still guard size.
size <= 0
i += 0 never advances. i += -1 walks backward. size = Infinity makes one giant slice (because i + size is Infinity and slice caps at length), which may be what you want or may be a bug. Requiring a positive integer deletes the whole class of loops. Number.isInteger(NaN) is false, so NaN throws too.
A looser API that returns [] for bad size hides caller bugs. Throw.
Holes and array-likes
slice copies holes as holes. chunk([1, , 3], 2) is [[1, empty], [3]]. map on those inner arrays will skip the hole. If the interviewer wants dense chunks, use a manual copy that writes undefined into missing indexes.
Array-likes ({ 0: 'a', 1: 'b', length: 2 }): Array.prototype.slice.call(arrayLike, i, i + size) works. Don’t claim chunk on a Set without [...set] first; sets have no integer index contract.
Chunk footguns
- Padding the last chunk with
nulls “so every row has length n.” That is a different function (padEndon chunks). Don’t. - Returning the original array when
size >= length. Always wrap:[[...]]viaslice, so the caller can mutate a chunk without mutatingarrat the top level. (Nested objects still alias.) sizeas a string.Number.isInteger('2')is false. Coerce withNumberif the prompt showed'2', otherwise throw.- Sparse
length.arr.lengthis the source of truth, including holes. Don’tfilterthem out as a surprise. - Sharing inner arrays.
out.push(arr)whensize >= lengthwithout slicing would leak the input as chunk zero.
This helper shows up next to groupBy in the same round: one partitions by key, one partitions by count. Keep them separate. Chunk does not look at values.
Interview out loud
“I loop with a stride of n and slice each window so the original array stays intact. The last slice is shorter when the length is not a multiple of n. Empty input returns an empty array of chunks, not one empty chunk. size that is not a positive integer throws, because zero would infinite-loop. I will not splice the input; slice is a shallow copy of references, so nested objects are still shared. If they want mutation, I will splice a copy and say that shifting from the front is extra work.”
Further reading
Related on this site
- Array map, filter, reduce
- Functional array patterns
- Implement groupBy
- Flatten a nested array
- Pure functions
- Rest and spread syntax
Related guides
- Array.filter PolyfillWrite filter that skips holes, respects thisArg, and never writes empty slots for rejected items. Contrast with map’s hole-preserving result.
- Array.find PolyfillWrite Array.prototype.find: first callback hit wins, holes are read as undefined, nothing matching returns undefined, and findIndex is the index twin.
- Array.flat PolyfillImplement flat(depth) with Infinity, holes, and non-array array-likes. Recursion vs stack, and why concat(spread) blows up on huge inputs.
- Array.forEach PolyfillImplement Array.prototype.forEach: visit present elements, skip holes, ignore the callback return, and honor thisArg without using native forEach.
- Array.includes PolyfillImplement Array.prototype.includes with SameValueZero: NaN is found, +0 equals -0, and fromIndex can walk backwards from the end.