Adding and Removing
Mental model: Work at the end of an array when you can: push and pop touch one slot, shift and unshift move every slot.
Level: beginner · about 10 minutes
const stack = ['a', 'b'];
console.log(stack.push('c')); // 3, the new length
console.log(stack.pop()); // 'c', the removed element
console.log(stack.unshift('z')); // 3, the new length
console.log(stack.shift()); // 'z', the removed element
console.log(stack); // ['a', 'b'], back where we startedAll four of these change the array in place and return something different.
These are mutating methods: they modify the array you called them on rather than returning a new one. That is fine when you own the array, and a source of confusing bugs when you were handed it by someone else. Note the asymmetry in return values: the adders give you the new length, the removers give you the element.
| Method | Does what | Returns | Cost |
|---|---|---|---|
push(x) | adds to the end | new length | O(1) |
pop() | removes from the end | the element | O(1) |
unshift(x) | adds to the front | new length | O(n) |
shift() | removes from the front | the element | O(n) |
splice(i, n, ...add) | removes and inserts anywhere | array of removed items | O(n) |
A stack is push plus pop
const history = [];
history.push('opened file');
history.push('typed a line');
history.push('deleted a line');
console.log(history.pop()); // 'deleted a line', most recent first
console.log(history.pop()); // 'typed a line'
console.log(history); // ['opened file']Last in, first out. Undo history, call stacks, bracket matching.
A queue is push plus shift
const jobs = [];
jobs.push('render');
jobs.push('upload');
jobs.push('email');
console.log(jobs.shift()); // 'render', oldest first
console.log(jobs.shift()); // 'upload'
console.log(jobs); // ['email']First in, first out. Print jobs, task runners, breadth-first search.
Queue with shift (fine, and simple)
const q = [1, 2, 3];
while (q.length) {
handle(q.shift()); // O(n) per call
}
Queue with a moving head (fast)
const q = [1, 2, 3];
let head = 0;
while (head < q.length) {
handle(q[head]); // O(1) per call
head += 1;
}Reach for the second form only when the queue is large or hot. Readability first, then measure, then optimise the thing you measured.
splice does everything, which is why it is confusing
const days = ['mon', 'tue', 'wed', 'thu'];
const removed = days.splice(1, 2); // from index 1, remove 2
console.log(removed); // ['tue', 'wed']
console.log(days); // ['mon', 'thu']
days.splice(1, 0, 'tue', 'wed'); // remove nothing, insert two
console.log(days); // ['mon', 'tue', 'wed', 'thu']
splice(i)removes everything fromionward.splice(i, 0, x)insertsxatiwithout removing anything.splice(i, 1, x)replaces the element ati.splice(-1, 1)removes the last element, likepop.
const nums = [1, 2, 2, 3];
for (let i = 0; i < nums.length; i++) {
if (nums[i] === 2) nums.splice(i, 1); // skips the second 2
}
console.log(nums); // [1, 2, 3], one 2 survived
console.log([1, 2, 2, 3].filter((n) => n !== 2)); // [1, 3], no index gamesThe classic off-by-one, and the two-character fix.
const a = ['a', 'b', 'c', 'd'];
const out = a.splice(1, 2, 'x');
console.log(out, a);splice returns the array of what it removed and mutates the original to hold the result. Two elements came out (b and c), one went in (x), so the array shrinks by one. This return value is the single most misremembered thing about splice.
Try it yourself
Stack against queue, same input
const input = ['first', 'second', 'third'];
const stack = [...input];
const queue = [...input];
console.log('stack order:');
while (stack.length) console.log(' ', stack.pop());
console.log('queue order:');
while (queue.length) console.log(' ', queue.shift());
console.log('the original is untouched:', input);
Swap shift for pop in the queue and watch the order flip. Then add a peek that reads without removing.
Exercises
A log that forgets
Write createLog(limit) returning an object with add(entry) and entries(). add appends and returns the current number of entries. Once there are more than limit entries, the oldest ones drop off the front. entries() returns a copy, oldest first, so callers cannot mutate your internal array.
Check yourself
- What does this log?
- 4 [1, 2, 3, 4] —
pushreturns the newlength, not the array and not the element. That is why chainingarr.push(x).push(y)fails: you would be callingpushon a number. - You process a 200,000 item queue with
while (q.length) handle(q.shift()). What is the total cost? - O(n squared), because each shift reindexes the rest — Each
shifthas to move every remaining element down one index, so n shifts do roughly n squared work. Keeping a head pointer, or usingpopwhen order does not matter, brings it back to O(n). - Which call inserts
"x"at index 2 without removing anything? arr.splice(2, 0, "x")— The second argument is the delete count, so0means pure insertion. With1you would replace the element at index 2, andslicenever inserts anything at all.
Common mistakes
- Expecting
pushto return the array, then trying to chain it. - Splicing inside a forward
forloop, which skips the element after each removal. - Reaching for
shiftin a hot loop over a large array.
Takeaways
push/popwork at the cheap end;shift/unshiftreindex everything.- Adders return the new length; removers return the removed element.
splicereturns what it removed and mutates in place.- Removing while looping forward skips elements. Filter into a new array instead.