30% offEnding soon

JavaScript Array slice(): Interview Guide

20 min read

JavaScript Array slice() returns a new array containing the half-open range from start up 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.

Included start, excluded end01234abcdestartend[1, 4)
The range starts at index 1 and stops at the boundary before index 4.

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'].

Count backward from length5 − 2 = 32 steps backabcde012345−2 starts here
Negative two means two boundary steps backward from length five.

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.

ExpressionNormalized rangeResult
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.

New containers, shared contentssource arrayobjectcopied arrayobjectone shared objectscore: 95
slice() copies the outer array, while object references remain shared.
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:

MethodSource arrayReturn valueTypical purpose
slice(start, end)Remains unchangedNew array containing the selected rangeCopy or read a range
splice(start, deleteCount, ...items)Changes in placeArray containing deleted elementsRemove, 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.

Same return, different sourcesource: a b c dsource: a b c dslice(1, 3)splice(1, 2)returns: b creturns: b cstill: a b c dnow: a d
slice() and splice() can return the same values but leave different sources.

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.

pageIndex × pageSize = start2 × 10 = 20page 010 itemspage 110 itemspage 2[20, 30)0102030
Zero-based page two begins after two complete blocks of ten items.

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.

Copy around the targeta bcd eslice(0, index)skipslice(index + 1)a b d e
Two slices copy around the target and meet again without it.

Rotate an array with two slices

Split once, swap the rangesA: before offseta bB: from offsetc d eB: c d eA: a b
Rotation moves the right range to the front while preserving order inside both ranges.

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:

  • items is an array.
  • size must be a positive integer.
  • The function returns consecutive chunks with at most size elements.
  • 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 slice window advances by size1234567[0, 3)[3, 6)[6, 9)1 2 34 5 67end clamps to 7start: 0 → 3 → 6 → stop
Each iteration advances three positions; the last requested range is clamped at seven.

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.

What happens when start reaches length?start = length = 66 < 6 is falsestop6 ≤ 6 is trueenter againslice(6, 9) → []
Stop when start reaches length; entering once more appends an empty chunk.

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, while splice() 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.

A hole is not an undefined valuesource1missingslot3slice()copy1stillabsent3undefined = present value
slice() preserves a hole as an absent index, not as an undefined value.

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.

Frequently asked questions

Does JavaScript slice() change the original array?
No. slice() returns a new array without changing the source array. The copy is shallow, so objects inside both arrays still refer to the same objects.
Is the end index included in Array slice()?
No. slice(start, end) includes the element at start and stops immediately before end. Thinking of the selection as the half-open range [start, end) prevents many boundary mistakes.
How do negative indexes work with JavaScript slice()?
A negative boundary is measured backward from the array length. For an array with five elements, slice(-2) starts at index three and returns the final two elements.
What is the difference between slice() and splice()?
slice() copies a range into a new array and leaves the source unchanged. splice() changes the source array by removing, replacing, or adding elements, then returns the deleted elements.
Does slice() create a deep copy of an array?
No. slice() creates a shallow copy. The outer array is new, but object elements are shared between the source and the result.