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().
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:
// 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
Runs on this device, in your browser. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs.
Your run, in this browser
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. Likepush(), it takes several items and keeps their order.- On an empty array,
pop()andshift()returnundefinedand change nothing, so a loop such aswhile (queue.length > 0)is the safe way to empty a queue whose items may themselves beundefined.
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:
// 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
Runs on this device, in your browser. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs.
Your run, in this browser
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:
// 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
Runs on this device, in your browser. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs.
Your run, in this browser
- Insert: a
deleteCountof 0 deletes nothing, soroute.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
startcounts from the end, sosplice(-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:
| Criterion | splice() | slice() |
|---|---|---|
| Changes the array | Yes | No |
| First argument | where to start | where to start |
| Second argument | how many items to delete | where to stop, not included |
| More arguments | items to insert | none |
| Returns | the deleted items, in an array | the copied items, in a new array |
| When to choose | Editing an array that only this code uses | Taking 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:
| Criterion | Changes the array | Returns 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 removed | the 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:
// 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.
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 indexfromhas moved to indexto; the other items keep their order andlistdoes 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, asat()does, somoveItem(['a', 'b', 'c'], -1, 0)returns['c', 'a', 'b']. An index that points outside the list throws aRangeError.removeAll(list, value)removes every item that is===tovaluefromlistitself, in place, and returns how many it removed. Afterconst tags = ['x', 'x', 'y', 'x'],removeAll(tags, 'x')returns 3 andtagsis['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.
Results of the sample tests
| Test | Result | Details |
|---|
What your code printed
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.
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.
-
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.
-
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.
Key takeaways
push()andpop()work at the end,unshift()andshift()at the front, andsplice()anywhere. All five change the array.push()andunshift()return the new length;pop()andshift()return the item they removed, orundefinedfor 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 aTypeError. splice(i, 0, x)inserts,splice(i, n)deletes,splice(i, n, x)replaces andsplice(i)deletes to the end.toSpliced()andslice()return new arrays and leave the original alone: use them when other code holds the array.shift()andunshift()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 withshift()and well under a millisecond with the index.
References
- ECMAScript 2026 Language Specification: Array.prototype.push (Ecma International)
- ECMAScript 2026 Language Specification: Array.prototype.shift (Ecma International)
- ECMAScript 2026 Language Specification: Array.prototype.unshift (Ecma International)
- ECMAScript 2026 Language Specification: Array.prototype.splice (Ecma International)
- ECMAScript 2026 Language Specification: Array.prototype.toSpliced (Ecma International)
- ECMAScript 2026 Language Specification: Array.prototype.slice (Ecma International)
- Array.prototype.splice() (MDN Web Docs)
- Array.prototype.toSpliced() (MDN Web Docs)
- Array.prototype.shift() (MDN Web Docs)
- heap.cc: Heap::LeftTrimFixedArray, the V8 code that shift() uses in Node.js 24.21.0 (OpenJS Foundation (Node.js repository))
- globals.h: kMaxRegularHeapObjectSize, the largest object outside the large-object space, in Node.js 24.21.0 (OpenJS Foundation (Node.js repository))
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress