Adding & Removing Elements
Arrays in JavaScript are dynamic, so you can grow or shrink them at any time without declaring a fixed size. The language gives you a small set of built-in methods for adding and removing elements, each targeting a specific position: the end, the start, or an arbitrary index. Knowing which method touches which end—and that most of them mutate the original array—is essential to writing predictable code.
Working at the end: push and pop
The end of an array is the cheapest place to add or remove items, because no other elements have to shift position. push() appends one or more elements to the end and returns the array’s new length. pop() removes the last element and returns it (or undefined if the array is empty).
const stack = ["a", "b"];
const newLength = stack.push("c", "d");
console.log(newLength); // 4
console.log(stack); // ["a", "b", "c", "d"]
const removed = stack.pop();
console.log(removed); // "d"
console.log(stack); // ["a", "b", "c"]
Output:
4
[ 'a', 'b', 'c', 'd' ]
'd'
[ 'a', 'b', 'c' ]
Together, push and pop make an array behave like a stack (last-in, first-out), which is handy for undo histories, parser state, and recursion-free traversals.
Working at the start: shift and unshift
unshift() adds one or more elements to the beginning of the array and returns the new length, while shift() removes the first element and returns it. Because every remaining element must be re-indexed, these operations are O(n)—noticeably slower than push/pop on large arrays.
const queue = ["b", "c"];
queue.unshift("a");
console.log(queue); // ["a", "b", "c"]
const first = queue.shift();
console.log(first); // "a"
console.log(queue); // ["b", "c"]
Output:
[ 'a', 'b', 'c' ]
'a'
[ 'b', 'c' ]
Pairing push with shift gives you a queue (first-in, first-out). For high-throughput queues, a real linked-list or ring-buffer structure avoids the re-indexing cost.
Inserting and removing anywhere: splice
splice() is the Swiss Army knife: it can remove, replace, and insert elements at any index, all in one call. Its signature is:
array.splice(start, deleteCount, item1, item2, ...)
start— the index to begin changing (negative counts from the end).deleteCount— how many elements to remove (omit to remove everything fromstartonward; use0to insert without removing).item1, item2, ...— optional elements to insert atstart.
splice() returns an array of the removed elements and mutates the original in place.
const colors = ["red", "green", "blue", "yellow"];
// Remove 2 elements starting at index 1
const removed = colors.splice(1, 2);
console.log(removed); // ["green", "blue"]
console.log(colors); // ["red", "yellow"]
// Insert without removing (deleteCount = 0)
colors.splice(1, 0, "orange", "purple");
console.log(colors); // ["red", "orange", "purple", "yellow"]
// Replace one element with another
colors.splice(0, 1, "crimson");
console.log(colors); // ["crimson", "orange", "purple", "yellow"]
Output:
[ 'green', 'blue' ]
[ 'red', 'yellow' ]
[ 'red', 'orange', 'purple', 'yellow' ]
[ 'crimson', 'orange', 'purple', 'yellow' ]
Tip: Don’t confuse
splice()withslice().slice()is non-mutating and returns a shallow copy of a portion of the array;splice()mutates and returns what it removed. See the dedicated slice vs splice page below.
Mutation matters
Every method on this page changes the array in place rather than returning a new one. If something else holds a reference to that array—a piece of React state, a cached value, a function argument—it sees the change too, which can cause subtle bugs.
const original = [1, 2, 3];
const alias = original;
original.push(4);
console.log(alias); // [1, 2, 3, 4] — the alias changed too
Output:
[ 1, 2, 3, 4 ]
When you need an unchanged copy, build a new array with the spread operator or non-mutating alternatives such as slice(), concat(), or the newer toSpliced() (ES2023), which works like splice() but returns a new array.
const base = [1, 2, 3];
const added = [...base, 4]; // add without mutating
const cut = base.toSpliced(0, 1); // remove without mutating
console.log(base); // [1, 2, 3] — untouched
console.log(added); // [1, 2, 3, 4]
console.log(cut); // [2, 3]
Output:
[ 1, 2, 3 ]
[ 1, 2, 3, 4 ]
[ 2, 3 ]
Method comparison
| Method | Position | What it does | Returns | Mutates? |
|---|---|---|---|---|
push() | End | Adds element(s) | New length | Yes |
pop() | End | Removes one element | Removed element | Yes |
unshift() | Start | Adds element(s) | New length | Yes |
shift() | Start | Removes one element | Removed element | Yes |
splice() | Any index | Inserts / removes / replaces | Array of removed items | Yes |
toSpliced() | Any index | Like splice | New array | No |
Best Practices
- Prefer
push/popovershift/unshiftin hot paths—operations at the end are O(1), while operations at the start are O(n). - Use
splice(index, 1)to remove a single element by position; combine withindexOf()to remove by value. - Reach for
toSpliced(), spread, orslice()when callers or framework state must not see in-place mutation. - Remember
splice()returns the removed elements, not the modified array—don’t chain off its return value expecting the whole array. - Quote and check
deleteCount: passing0inserts, omitting it removes everything fromstartto the end. - Avoid
delete array[i]—it leaves a hole (undefined) and does not shorten the array; usesplice()instead.