JavaScript Array slice() returns a new array containing the half-open range from
startup to, but excluding,end, while leaving the source array unchanged.
How JavaScript Array slice() Works
slice() copies a selected range of an array into a new array. It has three common signatures:
array.slice()
array.slice(start)
array.slice(start, end)
The clearest mental model is the half-open range [start, end). The square bracket means the start index is included. The parenthesis means the end index is excluded.
Consider this array:
const letters = ['a', 'b', 'c', 'd', 'e'];
letters.slice(1, 4) starts at index 1, which contains 'b'. It stops before index 4, which contains 'e'.
const letters = ['a', 'b', 'c', 'd', 'e'];
console.log(letters.slice(1, 4));
console.log(letters);
[ 'b', 'c', 'd' ]
[ 'a', 'b', 'c', 'd', 'e' ]
The result contains three elements because 4 - 1 equals 3. That subtraction works whenever both normalized boundaries fall within the array and end is greater than start.
Calling slice() without arguments copies the entire array. Supplying only start selects from that boundary through the end. Supplying both arguments selects [start, end).
The source remains unchanged in every case. This makes slice() useful when an interview solution must preserve its input. The result is a new outer array, although object elements inside it can still be shared.
If the task requires changing the array in place, Array.prototype.splice has a different contract.
Start, End, and Negative Index Rules
JavaScript first turns start and end into usable boundaries between 0 and the array length. A positive index counts from the beginning. A negative index counts backward from the length.
Keep using the same array:
const letters = ['a', 'b', 'c', 'd', 'e'];
Its length is 5. A boundary of -2 therefore becomes 5 + (-2), which is 3. The selection letters.slice(-2) behaves like letters.slice(3, 5) and returns ['d', 'e'].
Boundaries outside the array are clamped to the valid range from 0 through length. A start below the beginning becomes 0. An end beyond the array becomes length.
| Expression | Normalized range | Result |
|---|---|---|
letters.slice() | [0, 5) | ['a', 'b', 'c', 'd', 'e'] |
letters.slice(2) | [2, 5) | ['c', 'd', 'e'] |
letters.slice(1, 4) | [1, 4) | ['b', 'c', 'd'] |
letters.slice(-2) | [3, 5) | ['d', 'e'] |
letters.slice(1, -1) | [1, 4) | ['b', 'c', 'd'] |
letters.slice(-4, -1) | [1, 4) | ['b', 'c', 'd'] |
letters.slice(-99, 2) | [0, 2) | ['a', 'b'] |
letters.slice(3, 99) | [3, 5) | ['d', 'e'] |
letters.slice(2, 2) | [2, 2) | [] |
letters.slice(4, 2) | [4, 2) | [] |
letters.slice(99) | [5, 5) | [] |
An equal or reversed range is empty. After normalization, if end is at or before start, there are no indexes inside [start, end).
Try predicting these results before reading the output:
const letters = ['a', 'b', 'c', 'd', 'e'];
console.log(letters.slice(0, 1));
console.log(letters.slice(-3, 4));
console.log(letters.slice(-1, -3));
[ 'a' ]
[ 'c', 'd' ]
[]
The second expression normalizes -3 to 2, so its range is [2, 4). The third normalizes to [4, 2), which is empty.
Shallow Copies, Mutation, and slice() vs. splice()
A shallow copy creates a new outer array without cloning the objects stored inside it. The two arrays are distinct, but an object element in each array points to the same object.
const candidates = [
{ name: 'Ari', score: 70 },
{ name: 'Bo', score: 80 },
];
const copied = candidates.slice();
copied[0].score = 95;
console.log(copied === candidates);
console.log(candidates[0].score);
false
95
Replacing an array position tells a different story. The outer arrays have separate positions, so assigning copied[0] does not replace candidates[0]. Mutating the shared object does affect what both arrays observe.
slice() and splice() look similar enough to cause interview errors, but their contracts differ:
| Method | Source array | Return value | Typical purpose |
|---|---|---|---|
slice(start, end) | Remains unchanged | New array containing the selected range | Copy or read a range |
splice(start, deleteCount, ...items) | Changes in place | Array containing deleted elements | Remove, replace, or add elements |
const sourceForSlice = ['a', 'b', 'c', 'd'];
const sourceForSplice = ['a', 'b', 'c', 'd'];
const sliced = sourceForSlice.slice(1, 3);
const spliced = sourceForSplice.splice(1, 2);
console.log(sliced, sourceForSlice);
console.log(spliced, sourceForSplice);
[ 'b', 'c' ] [ 'a', 'b', 'c', 'd' ]
[ 'b', 'c' ] [ 'a', 'd' ]
Both return ['b', 'c'] here. Only splice() changes its source.
Array Slicing Patterns Used in Interviews
Several interview patterns reduce to calculating the two boundaries of [start, end).
Take the first elements with slice()
To take the first count elements, start at 0 and use count as the exclusive end:
const scores = [12, 18, 25, 31, 44];
const firstThree = scores.slice(0, 3);
The range [0, 3) contains indexes 0, 1, and 2.
Drop the first elements with slice()
To skip count elements, use count as the start and omit end:
const scores = [12, 18, 25, 31, 44];
const afterTwo = scores.slice(2);
This pattern appears when processing a header separately from the remaining values.
Paginate an array with slice()
For a zero-based pageIndex and a pageSize, calculate:
const start = pageIndex * pageSize;
const end = start + pageSize;
const page = items.slice(start, end);
For pageIndex = 2 and pageSize = 10, the range is [20, 30). If fewer elements remain, slice() stops at the array length.
Remove one element without mutating the array
Copy everything before the target index, copy everything after it, then combine those ranges:
function removeAt(items, index) {
return items.slice(0, index).concat(items.slice(index + 1));
}
The first range ends before index. The second starts after it. Array.prototype.concat joins the two new arrays.
Rotate an array with two slices
A left rotation at offset moves [0, offset) behind [offset, length):
function rotateLeft(items, offset) {
return items.slice(offset).concat(items.slice(0, offset));
}
This compact version assumes the caller supplies a boundary suitable for the array. An interview follow-up may ask for normalization when the offset is negative or exceeds the length.
Divide an array into chunks
Chunking repeatedly takes [start, start + size), then advances start by size. The last range can extend past the array length because slice() clamps its end boundary.
This pattern pairs naturally with Array.prototype.map in some functional solutions, but a loop makes the boundary calculation and progress condition easier to inspect. Broader timed practice is available in the JavaScript coding interview guide, with additional workspaces in UIReady Premium Annual.
Build and Test chunkArray() with Jest
Define the helper’s contract before writing it:
itemsis an array.sizemust be a positive integer.- The function returns consecutive chunks with at most
sizeelements. - An empty input returns an empty array.
- A final incomplete chunk is included.
- The source array is not mutated.
- Zero, negative, fractional, and nonnumeric sizes cause a
RangeError.
JavaScript does not prescribe this contract for a custom chunkArray() helper. Throwing for invalid sizes is a deliberate choice that prevents a loop from stalling.
The loop needs one changing boundary, start. On each iteration, it copies [start, start + size) and then increases start by size.
Here is the canonical implementation:
function chunkArray(items, size) {
if (!Number.isInteger(size) || size <= 0) {
throw new RangeError('size must be a positive integer');
}
const chunks = [];
for (let start = 0; start < items.length; start += size) {
chunks.push(items.slice(start, start + size));
}
return chunks;
}
module.exports = { chunkArray };
When items.length is 7 and size is 3, the loop requests [0, 3), [3, 6), and [6, 9). The last end boundary is clamped to 7, so the final chunk contains one element.
The following Jest suite checks the returned chunks and preserves a snapshot of each nonempty source array:
const { chunkArray } = require('./chunkArray');
describe('chunkArray', () => {
test('creates chunks of the requested size', () => {
const input = [1, 2, 3, 4, 5, 6];
const original = input.slice();
expect(chunkArray(input, 2)).toEqual([
[1, 2],
[3, 4],
[5, 6],
]);
expect(input).toEqual(original);
});
test('includes a partial final chunk', () => {
const input = [1, 2, 3, 4, 5];
const original = input.slice();
expect(chunkArray(input, 2)).toEqual([
[1, 2],
[3, 4],
[5],
]);
expect(input).toEqual(original);
});
test('returns an empty array for empty input', () => {
const input = [];
const original = input.slice();
expect(chunkArray(input, 3)).toEqual([]);
expect(input).toEqual(original);
});
test.each([0, -1, 1.5, NaN])(
'rejects invalid size %s',
(size) => {
const input = [1, 2, 3];
const original = input.slice();
expect(() => chunkArray(input, size)).toThrow(RangeError);
expect(input).toEqual(original);
}
);
});
The non-mutation assertions matter because correct chunks alone do not prove that the helper preserved its input. A solution could remove elements with splice() and still return the expected groups.
Another common bug uses start <= items.length. When the length is an exact multiple of size, that condition performs one extra iteration and appends an empty chunk. The correct condition is start < items.length.
Interview Traps and Follow-Up Questions
The most common boundary mistake is treating end as inclusive. slice(1, 3) returns indexes 1 and 2, not index 3. Ask which indexes belong to [1, 3) before predicting the values.
A second mistake is reading the second argument as a count. In slice(4, 7), 7 is the exclusive end boundary. The selection has three elements only because 7 - 4 equals 3.
Interviewers also probe these cases:
slice()preserves the source array, whilesplice()changes it.- A copied outer array does not imply cloned object elements.
- Equal or reversed normalized boundaries produce an empty array.
- Negative boundaries are measured from the array length and then clamped.
- A chunking loop must advance on every iteration.
Sparse arrays add one follow-up. A sparse array has missing indexes, called empty slots. slice() preserves those slots in the selected range rather than converting each one to an explicit undefined value.
const sparse = [1, , 3];
const copied = sparse.slice();
console.log(copied.length);
console.log(1 in copied);
3
false
The result has length 3, but index 1 is still absent.
slice() can also be called on an array-like object. An array-like object has a length property and indexed properties, although it is not necessarily an array.
const values = {
0: 'red',
1: 'green',
2: 'blue',
length: 3,
};
const result = Array.prototype.slice.call(values, 1, 3);
console.log(result);
console.log(Array.isArray(result));
[ 'green', 'blue' ]
true
For a broader array transformation question, an interviewer may ask how the solution relates to Array.prototype.flat. chunkArray() groups one array into nested arrays, while flat() performs the opposite style of transformation by reducing nesting.