Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

JavaScript (ES2026)  Module 6 – Arrays and array methods

Adding and removing items

Add and remove JavaScript array items with push, pop, shift, unshift and splice, know what each returns, and see why long queues should not use shift().

  • Beginner
  • 15 minutes
  • Examples run with Node.js 24.21.0 and quickjs 0.32.0
  • By MySmartCoPilot
Free lesson, Pro answers: 15 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

What you will learn

  • Use push, pop, shift, unshift and splice correctly
  • Explain which methods mutate and what they return
  • Choose data structures when you need many removals from the front

Before you start

What you’ll learn · Before you start

What you will learn

  • Use push, pop, shift, unshift and splice correctly
  • Explain which methods mutate and what they return
  • Choose data structures when you need many removals from the front

Before you start

On this page

Four methods add and remove items at the ends of an array: push() adds items at the end and pop() removes the last one, unshift() adds items at the front and shift() removes the first one. splice() works anywhere: one call can insert, delete or replace items at any index. All five change the array they are called on, and they return different things: push() and unshift() return the new length, pop() and shift() the item they removed, and splice() an array of the items it deleted. Mixing those up is a classic bug. This lesson runs each method, shows the copying versions that leave the original alone, toSpliced() and slice(), and measures why a long queue should never be emptied with shift().

push, pop, shift and unshift

A queue at a ticket window is an array where people join at the back and leave from the front:

A ticket-window queue, one change at a time JavaScript · platform-queue.mjs
// The queue at a station's ticket window: people join at the back with push() and the window serves the front
// with shift(). Each method changes the array and returns something different.
const queue = ['Asha', 'Ravi'];

console.log('push returns the new length:', queue.push('Meena'));
console.log('push can add several:', queue.push('Kiran', 'Arjun'), queue);

console.log('shift returns who was served:', queue.shift(), queue);
console.log('pop returns who left the back:', queue.pop(), queue);
console.log('unshift returns the new length:', queue.unshift('Divya'), queue);

const nobody = [];
console.log('pop and shift on an empty array:', nobody.pop(), nobody.shift(), nobody.length);

Output

push returns the new length: 3
push can add several: 5 [ 'Asha', 'Ravi', 'Meena', 'Kiran', 'Arjun' ]
shift returns who was served: Asha [ 'Ravi', 'Meena', 'Kiran', 'Arjun' ]
pop returns who left the back: Arjun [ 'Ravi', 'Meena', 'Kiran' ]
unshift returns the new length: 4 [ 'Divya', 'Ravi', 'Meena', 'Kiran' ]
pop and shift on an empty array: undefined undefined 0

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node platform-queue.mjs

  • push(item) adds at the end and returns the new length, 3. It takes any number of items, push('Kiran', 'Arjun'), and adds them in that order.
  • shift() removes the first item and returns it: the window serves Asha, and everyone else moves up one place.
  • pop() removes the last item and returns it, here Arjun, who gave up waiting.
  • unshift(item) adds at the front and returns the new length. Like push(), it takes several items and keeps their order.
  • On an empty array, pop() and shift() return undefined and change nothing, so a loop such as while (queue.length > 0) is the safe way to empty a queue whose items may themselves be undefined.
Diagram: shift() and unshift() work at the front of an array, where every item moves; push() and pop() at the back.FRONTshift() removes, unshift() addsevery other item gets a new indexqueueBACKpop() removes, push() addsno other item moves'Ravi'[0]'Meena'[1]'Kiran'[2]'Arjun'[3]at index 0at the end

Which end each method works on

Text description of the diagram

The diagram shows a queue of four items, 'Ravi' at index 0, 'Meena' at index 1, 'Kiran' at index 2 and 'Arjun' at index 3.

  • Above it, a box for the front of the array points to the queue, at index 0: shift() removes the first item and unshift() adds items before it. Either way every other item gets a new index, one higher or one lower.
  • Below it, the queue points to a box for the back of the array, after index 3: pop() removes the last item and push() adds items after it. No other item moves.

The two ends are not equal. At the back, push() and pop() touch one slot and leave every other item where it was. At the front, every other item gets a new index: after shift(), Ravi moves from index 1 to index 0, Meena from 2 to 1, and so on down the queue. For a few hundred items you will never notice; a later section of this lesson measures what it costs for a long queue.

What each method returns

All four methods return something useful, but never the array itself, and that trips people up when they try to chain calls or keep the result:

The result of push() and of splice() JavaScript · return-values.mjs
// push() returns a number, not the array, so its result cannot stand in for the array.
const cart = ['rice'];
const result = cart.push('tea');
console.log('result:', result, typeof result);

try {
  cart.push('salt').push('oil');
} catch (error) {
  console.log(error.name, 'from chaining push(); cart is now', cart);
}

// splice() returns what it removed, not what is left.
const playlist = ['intro', 'song 1', 'ad', 'song 2'];
const removed = playlist.splice(2, 1);
console.log('splice returned', removed, 'and the playlist is', playlist);

Output

result: 2 number
TypeError from chaining push(); cart is now [ 'rice', 'tea', 'salt' ]
splice returned [ 'ad' ] and the playlist is [ 'intro', 'song 1', 'song 2' ]

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node return-values.mjs

cart.push('tea') returns 2, the new length, so result is a number. That is also why cart.push('salt').push('oil') fails: the first call returns 3, and a number has no push() method, so the second call throws a TypeError. Note that the first push() already ran, so 'salt' is in the cart even though the line failed. Call the methods one per line instead.

splice() returns the items it removed, in an array of their own, even when it removes only one: ['ad'], not 'ad'. Code such as const remaining = playlist.splice(2, 1) reads as if it kept what is left, but remaining holds the advert and playlist holds the songs. When you want the removed item itself, take it out of that array: const [removed] = playlist.splice(2, 1).

splice(): insert, delete or replace anywhere

splice(start, deleteCount, ...items) goes to index start, deletes deleteCount items there and inserts items in their place, all in one call. Its two numbers cover every edit of a list:

Editing a bus route with splice(), and copying with toSpliced() JavaScript · splice-cases.mjs
// A bus route as an array of stops, changed in place with splice() and copied with toSpliced().
const route = ['Depot', 'Market', 'School', 'Hospital', 'Station'];

console.log('insert:', route.splice(2, 0, 'Library'), route);
console.log('delete:', route.splice(3, 1), route);
console.log('replace:', route.splice(-1, 1, 'Central'), route);
console.log('to the end:', route.splice(3), route);

const plan = ['Depot', 'Market', 'School'];
console.log('toSpliced:', plan.toSpliced(1, 1, 'Mall', 'Park'), plan);
console.log('slice:', plan.slice(1), plan.slice(0, -1), plan);

Output

insert: [] [ 'Depot', 'Market', 'Library', 'School', 'Hospital', 'Station' ]
delete: [ 'School' ] [ 'Depot', 'Market', 'Library', 'Hospital', 'Station' ]
replace: [ 'Station' ] [ 'Depot', 'Market', 'Library', 'Hospital', 'Central' ]
to the end: [ 'Hospital', 'Central' ] [ 'Depot', 'Market', 'Library' ]
toSpliced: [ 'Depot', 'Mall', 'Park', 'School' ] [ 'Depot', 'Market', 'School' ]
slice: [ 'Market', 'School' ] [ 'Depot', 'Market' ] [ 'Depot', 'Market', 'School' ]

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node splice-cases.mjs

  • Insert: a deleteCount of 0 deletes nothing, so route.splice(2, 0, 'Library') puts the new stop at index 2 and moves the rest up. It returns [], because nothing was removed.
  • Delete: route.splice(3, 1) removes one item at index 3 and returns it inside an array.
  • Replace: delete and insert together. A negative start counts from the end, so splice(-1, 1, 'Central') replaces the last stop.
  • Delete to the end: with no deleteCount, splice(3) removes everything from index 3 on.

toSpliced(), added in ECMAScript 2023, takes the same arguments and returns a new array with the edit made, leaving the original untouched: plan still has its three stops afterwards. slice(start, end) is the other copying method: it returns the items from start up to, but not including, end, and a negative number counts from the end, so plan.slice(0, -1) is everything but the last item.

Version note

toSpliced() needs Node.js 20 or later, Chrome 110, Firefox 115 or Safari 16. Where it is missing, the call throws a TypeError; const copy = list.slice(); copy.splice(…) does the same job there.

splice, toSpliced or slice?

The names look alike, and the difference that matters is whether the array changes:

splice() or slice()
Criterion splice()slice()
Changes the array YesNo
First argument where to startwhere to start
Second argument how many items to deletewhere to stop, not included
More arguments items to insertnone
Returns the deleted items, in an arraythe copied items, in a new array
When to choose Editing an array that only this code usesTaking a part of an array

toSpliced() sits between the two: it takes the arguments of splice() and, like slice(), returns a new array and leaves the original as it was, so it is the choice for an edit when other code also holds the array.

Changing an array in place is quick and fine when only one piece of code uses it. When other code holds the same array, such as the state of a user interface or an argument a function was given, an edit in place changes their data behind their back. Copying methods keep every other holder’s view as it was:

Change the array, or make a new one?
Criterion Changes the arrayReturns a new array
Add at the end list.push(x)[...list, x]
Add at the front list.unshift(x)[x, ...list]
Remove the last item list.pop()list.slice(0, -1)
Remove the first item list.shift()list.slice(1)
Edit the middle list.splice(i, n, ...items)list.toSpliced(i, n, ...items)
Returns the new length, or what was removedthe new array

Emptying a long queue with shift()

The specification defines shift() as copying every remaining item one index down and then shortening the array by one. Emptying a queue of n items that way could move about n × n ÷ 2 items in total. This program measures it, against a head index: a number that remembers where the front of the queue is, so that serving a job only reads queue[head] and adds one to head:

shift() against a head index, for queues of 10,000 to 80,000 jobs JavaScript · queue-drain.mjs
// Emptying a queue of n jobs from the front: shift() on every turn, against a head index that moves along the
// array. The queues grow with push(), as real queues do. Times are in milliseconds; the head index is timed three
// times and the fastest run is shown, because it is too quick to time once.
function makeQueue(n) {
  const queue = [];
  for (let i = 0; i < n; i++) queue.push(`job ${i}`);
  return queue;
}

function drainWithShift(queue) {
  let served = 0;
  while (queue.length > 0) {
    if (queue.shift() !== undefined) served++;
  }
  return served;
}

function drainWithHead(queue) {
  let served = 0;
  let head = 0;
  while (head < queue.length) {
    if (queue[head] !== undefined) served++;
    head++;
  }
  return served;
}

function time(drain, n, runs) {
  let best = Infinity;
  for (let run = 0; run < runs; run++) {
    const queue = makeQueue(n);
    const start = performance.now();
    if (drain(queue) !== n) throw new Error('a job was lost');
    best = Math.min(best, performance.now() - start);
  }
  return best;
}

console.log('     items      shift()   head index');
for (const n of [10_000, 15_000, 20_000, 40_000, 80_000]) {
  const shift = time(drainWithShift, n, 1).toFixed(1).padStart(8);
  const head = time(drainWithHead, n, 3).toFixed(2).padStart(7);
  console.log(`${n.toLocaleString('en-US').padStart(10)}  ${shift} ms  ${head} ms`);
}

Output

     items      shift()   head index
    10,000       2.2 ms     0.07 ms
    15,000       0.7 ms     0.02 ms
    20,000      52.2 ms     0.01 ms
    40,000     219.2 ms     0.03 ms
    80,000     854.7 ms     0.06 ms

This output changes from run to run: the times depend on the computer and on what else it is running

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node queue-drain.mjs

Up to 15,000 jobs, shift() emptied the queue in a millisecond or two. At 20,000 it took tens of milliseconds, and each doubling after that made it about four times slower, roughly a second at 80,000, while the head index stayed under a tenth of a millisecond at every size. The sudden jump between 15,000 and 20,000 comes from V8, the engine inside Node.js and Chrome. For an array of a few thousand items V8 usually avoids the copy: it moves the point where the array’s storage starts by one slot. Its source code says that this trick does not work for objects in its large-object space, and an array’s storage goes there once it needs more than 128 KiB, which is about 16,000 slots of 8 bytes. A queue that grew with push() has reserved spare slots as it grew, so it crosses that line a little sooner. Above it, every shift() really does copy every item that is left.

unshift() has the same problem in the other direction, and on every call: it moves every item up to make room at the front. Arrays therefore make good stacks, where items come and go at the end, and poor long-lived queues, which take their items from the front. For a queue, keep a head index as the program does, and when the served part grows large, drop it in one go with queue.splice(0, head) and set head back to 0; the data structures module builds a proper queue class.

JavaScript & TypeScript Online Compiler Run splice-cases.mjs with your own stops, then try the same edits with toSpliced() and compare the arrays.

Exercises

Exercise · Easy · JavaScript

Move an item into a new array, and remove every match in place

Write two functions in listops.mjs. One must leave its input alone, and the other must change it.

  • moveItem(list, from, to) returns a new array in which the item at index from has moved to index to; the other items keep their order and list does not change. moveItem(['a', 'b', 'c', 'd'], 0, 2) returns ['b', 'c', 'a', 'd']. Both indexes may be negative and then count from the end, as at() does, so moveItem(['a', 'b', 'c'], -1, 0) returns ['c', 'a', 'b']. An index that points outside the list throws a RangeError.
  • removeAll(list, value) removes every item that is === to value from list itself, in place, and returns how many it removed. After const tags = ['x', 'x', 'y', 'x'], removeAll(tags, 'x') returns 3 and tags is ['y'].

The starter's moveItem() changes the list it is given, and its removeAll() skips an item whenever two matches sit next to each other. Run the sample tests when you are done.

Starter code · listops.mjs

// The list helpers of lesson javascript/adding-and-removing-items.

export function moveItem(list, from, to) {
  // Moves the item, but changes the list it was given, and accepts any index.
  const [item] = list.splice(from, 1);
  list.splice(to, 0, item);
  return list;
}

export function removeAll(list, value) {
  // Skips the second of two neighbours that match: after a splice(), the next item moves into index i.
  let removed = 0;
  for (let i = 0; i < list.length; i++) {
    if (list[i] === value) {
      list.splice(i, 1);
      removed++;
    }
  }
  return removed;
}
The sample tests · listops.test.mjs
import { test, assert } from 'mysmartcopilot:test';
import { moveItem, removeAll } from './listops.mjs';

test('moves an item forward', () => {
  assert.deepEqual(moveItem(['a', 'b', 'c', 'd'], 0, 2), ['b', 'c', 'a', 'd']);
});

test('moves an item back', () => {
  assert.deepEqual(moveItem(['a', 'b', 'c', 'd'], 3, 1), ['a', 'd', 'b', 'c']);
});

test('returns a new array and leaves the list as it was', () => {
  const list = ['a', 'b', 'c'];
  const moved = moveItem(list, 0, 2);
  assert.deepEqual(moved, ['b', 'c', 'a']);
  assert.deepEqual(list, ['a', 'b', 'c']);
});

test('negative indexes count from the end; outside the list is a RangeError', () => {
  assert.deepEqual(moveItem(['a', 'b', 'c'], -1, 0), ['c', 'a', 'b']);
  assert.deepEqual(moveItem(['a', 'b', 'c'], 0, -1), ['b', 'c', 'a']);
  assert.throws(() => moveItem(['a', 'b'], 2, 0), RangeError);
  assert.throws(() => moveItem(['a', 'b'], 0, -3), RangeError);
});

test('removeAll removes neighbours that match, in place', () => {
  const tags = ['x', 'x', 'y', 'x'];
  assert.equal(removeAll(tags, 'x'), 3);
  assert.deepEqual(tags, ['y']);
});

test('removeAll returns 0 and changes nothing when no item matches', () => {
  const ids = [1, 2, 3];
  assert.equal(removeAll(ids, 9), 0);
  assert.deepEqual(ids, [1, 2, 3]);
});
A hint

For moveItem(), turn a negative index into a positive one first (list.length + index), check that both are between 0 and list.length - 1, then work on a copy, list.slice(): splice() the item out of the copy and splice() it back in at to. For removeAll(), walk the list from the last index down to 0, so that a splice() only moves items you have already looked at.

The sample tests run on this device, in your browser (QuickJS): nothing is sent to mysmartcopilot.com. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.

Check yourself

6 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.

  1. Question 1 of 6 What does this program print?

    Read the code, then choose one answer.

    const queue = ['a', 'b'];
    const n = queue.push('c');
    
    console.log(n, queue.shift(), queue);
    Show the answer to question 1

    Answer: 3 a [ 'b', 'c' ]

    push() returns the new length, 3, not the array. shift() removes and returns the first item, 'a', and the array that is printed afterwards holds the other two.

  2. Question 2 of 6 Which method adds items to the front of an array?

    Choose one answer.

    Show the answer to question 2

    Answer: unshift()

    unshift() adds items before index 0 and returns the new length. shift() removes the first item; push() and pop() work at the end.

  3. Question 3 of 6 What does this program print?

    Read the code, then choose one answer.

    const xs = [1, 2, 3, 4, 5];
    const removed = xs.splice(1, 2);
    
    console.log(removed, xs);
    Show the answer to question 3

    Answer: [ 2, 3 ] [ 1, 4, 5 ]

    The second argument of splice() is how many items to delete, starting at index 1, so 2 and 3 go. splice() changes xs and returns the deleted items in a new array.

  4. Question 4 of 6 Which calls leave the array list exactly as it was? Choose every right one.

    Choose every answer that is right.

    Show the answer to question 4

    Answer:

    • list.concat(4)
    • list.slice(1)
    • list.toSpliced(0, 1)

    slice(), toSpliced() and concat() build and return a new array. splice() and pop() change list itself and return what they removed.

  5. Question 5 of 6 What does splice-cases.mjs print?

    What does this program print? Choose one answer.

    // A bus route as an array of stops, changed in place with splice() and copied with toSpliced().
    const route = ['Depot', 'Market', 'School', 'Hospital', 'Station'];
    
    console.log('insert:', route.splice(2, 0, 'Library'), route);
    console.log('delete:', route.splice(3, 1), route);
    console.log('replace:', route.splice(-1, 1, 'Central'), route);
    console.log('to the end:', route.splice(3), route);
    
    const plan = ['Depot', 'Market', 'School'];
    console.log('toSpliced:', plan.toSpliced(1, 1, 'Mall', 'Park'), plan);
    console.log('slice:', plan.slice(1), plan.slice(0, -1), plan);
    Show the answer to question 5

    Answer: it prints

    insert: [] [ 'Depot', 'Market', 'Library', 'School', 'Hospital', 'Station' ]
    delete: [ 'School' ] [ 'Depot', 'Market', 'Library', 'Hospital', 'Station' ]
    replace: [ 'Station' ] [ 'Depot', 'Market', 'Library', 'Hospital', 'Central' ]
    to the end: [ 'Hospital', 'Central' ] [ 'Depot', 'Market', 'Library' ]
    toSpliced: [ 'Depot', 'Mall', 'Park', 'School' ] [ 'Depot', 'Market', 'School' ]
    slice: [ 'Market', 'School' ] [ 'Depot', 'Market' ] [ 'Depot', 'Market', 'School' ]

    splice() always returns the items it deleted (none when it only inserts) and changes the route in place: a negative start counts from the end, and leaving out the count deletes everything from the start index on. toSpliced() and slice() return new arrays and leave plan as it was.

  6. Question 6 of 6 Why does emptying a queue of 80,000 items with shift() take so much longer than with a head index?

    Choose one answer.

    Show the answer to question 6

    Answer: Each shift() moves every remaining item down one index, so the work grows with the square of the length

    The language defines shift() as moving every later item one index down. V8 skips that copying for small arrays, but not for long ones, so each call touches every item left in the queue. A head index only reads one item per turn.

Interview questions

Fresher · What is the difference between slice() and splice() in JavaScript?

slice(start, end) copies a part of an array into a new array, from start up to but not including end, and never changes the original. splice(start, deleteCount, ...items) changes the array itself: it deletes deleteCount items at start, inserts any items there, and returns the deleted items in a new array. So the second argument means “where to stop” for slice() and “how many to delete” for splice(). Both accept a negative start, which counts from the end. Use slice() to take a part without side effects, splice() to edit a list in place, and toSpliced(), from ECMAScript 2023, when you want the edit made on a copy.

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Mid to senior

    A Node.js worker keeps pending jobs in an array: push() to add, shift() to take the next. After an outage 80,000 jobs pile up, and draining them keeps a CPU core busy for about a second, while a test queue of 10,000 drained in a millisecond. Explain the difference and design a queue that stays fast.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Fresher to mid

    A helper reads function removeItem(cart, index) { return cart.splice(index, 1); }, and the screen calls setCart(removeItem(cart, i)). Removing a product leaves the cart showing only that product, and another screen that shares the same cart array loses items too. Explain both bugs and fix the helper.

    Model answer, Pro. First step and follow-ups

Key takeaways

  • push() and pop() work at the end, unshift() and shift() at the front, and splice() anywhere. All five change the array.
  • push() and unshift() return the new length; pop() and shift() return the item they removed, or undefined for an empty array; splice() returns the removed items in an array.
  • None of them returns the array, so do not chain them: list.push(a).push(b) throws a TypeError.
  • splice(i, 0, x) inserts, splice(i, n) deletes, splice(i, n, x) replaces and splice(i) deletes to the end.
  • toSpliced() and slice() return new arrays and leave the original alone: use them when other code holds the array.
  • shift() and unshift() move every other item. For a long queue, read with a head index instead: in the run above, 80,000 jobs took roughly a second with shift() and well under a millisecond with the index.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.