🛠️ Array Methods (push, pop, shift, etc.)
JavaScript arrays ship with dozens of built-in methods for adding, removing, reordering, and searching their contents. The single most important thing to learn is which methods mutate the original array and which return a fresh one — get that wrong and you'll chase phantom bugs for hours.
🎯 Learning Objectives
By the end of this lesson, you will be able to:
- Use
push/popfor stack behavior andshift/unshiftfor queue behavior - Add, remove, and replace elements anywhere with
splice() - Sort correctly with a compare function and understand why
sort()defaults to string order - Copy and combine arrays with the non-mutating
slice()andconcat() - Search arrays with
indexOf,includes,find, andfindIndex - Classify any method as a mutator or an accessor before you call it
Estimated Time: 35–45 minutes • Difficulty: Intermediate
Hands-on: Build a small playlist manager that adds, removes, reorders, and searches songs.
In This Lesson
Mutators vs. Accessors
Every array method falls into one of two camps. Mutator methods change the array they're called on. Accessor methods leave the original untouched and hand back a new array or a value. Knowing which is which is the key to writing predictable code.
change the original] A --> C[Accessors
return new data] B --> B1["push, pop, shift, unshift"] B --> B2["splice, sort, reverse, fill"] C --> C1["slice, concat, join"] C --> C2["indexOf, includes, find"]
📖 Why it matters
If you pass an array to a function and it calls sort() or splice(), the caller's array changes too — often unexpectedly. When you need to preserve the original, copy first with [...arr] or arr.slice(), then mutate the copy. Newer non-mutating twins (toSorted, toReversed, toSpliced, with) exist for exactly this reason.
Stack Operations: push & pop
A stack is a Last-In-First-Out (LIFO) structure — think of a stack of plates where you add and remove from the top. push() adds to the end, pop() removes from the end. Both operate on the "top" of the array and are very fast.
push() and pop() act on the top (the end of the array).const stack = [];
stack.push("First"); // ["First"]
stack.push("Second", "Third"); // ["First", "Second", "Third"] — push takes multiple args
const len = stack.push("Fourth"); // push RETURNS the new length
console.log(len); // 4
const top = stack.pop(); // pop RETURNS the removed element
console.log(top); // "Fourth"
console.log(stack); // ["First", "Second", "Third"]
console.log([].pop()); // undefined — popping an empty array is safe
A real-world LIFO use case is undo/redo: each edit pushes the previous state onto a history stack; undo pops it back.
class TextEditor {
content = "";
#history = [];
#redo = [];
type(text) {
this.#history.push(this.content); // save state for undo
this.content += text;
this.#redo = []; // a new edit invalidates the redo stack
}
undo() {
if (this.#history.length === 0) return;
this.#redo.push(this.content);
this.content = this.#history.pop();
}
redo() {
if (this.#redo.length === 0) return;
this.#history.push(this.content);
this.content = this.#redo.pop();
}
}
const editor = new TextEditor();
editor.type("Hello ");
editor.type("world!");
editor.undo(); // content → "Hello "
editor.redo(); // content → "Hello world!"
console.log(editor.content); // "Hello world!"
Queue Operations: shift & unshift
A queue is a First-In-First-Out (FIFO) structure — like a line at a checkout. Combine push() (add to the end) with shift() (remove from the front) to model one. unshift() adds to the front.
const queue = [];
queue.push("First");
queue.push("Second");
queue.push("Third"); // ["First", "Second", "Third"]
const next = queue.shift(); // removes & returns the FRONT element
console.log(next); // "First"
console.log(queue); // ["Second", "Third"]
queue.unshift("New First"); // adds to the front
console.log(queue); // ["New First", "Second", "Third"]
const newLen = queue.unshift("Zero"); // unshift RETURNS the new length
console.log(newLen); // 4
⚠️ Performance note
push() and pop() are O(1) — they touch only the end. shift() and unshift() are O(n) because every remaining element must be re-indexed. For a large, frequently-dequeued queue, consider a purpose-built structure (a linked list, or an index pointer that walks forward) instead of repeatedly calling shift().
splice — The Swiss Army Knife
splice() is the one method that can add, remove, and replace elements at any position — in place. Its signature is array.splice(start, deleteCount, ...itemsToInsert), and it returns an array of whatever it removed.
const months = ["Jan", "March", "April", "June"];
// INSERT (deleteCount 0): add "Feb" at index 1
months.splice(1, 0, "Feb");
console.log(months); // ["Jan", "Feb", "March", "April", "June"]
// REPLACE: remove 1 at index 4, insert "May"
months.splice(4, 1, "May");
console.log(months); // ["Jan", "Feb", "March", "April", "May"]
// REMOVE: delete 2 items starting at index 2
const removed = months.splice(2, 2);
console.log(months); // ["Jan", "Feb", "May"]
console.log(removed); // ["March", "April"] — splice returns what it cut
// A negative start counts from the end
const nums = [1, 2, 3, 4, 5];
nums.splice(-2, 1); // remove 1 element at the second-to-last spot
console.log(nums); // [1, 2, 3, 5]
📖 Don't confuse splice with slice
splice() mutates and returns removed items. slice() copies a range and leaves the original alone. The extra "p" is worth remembering.
sort & reverse
sort() reorders the array in place. Its most notorious gotcha: with no arguments it converts every element to a string and sorts by UTF-16 code units — so numbers sort alphabetically.
// THE CLASSIC TRAP — string comparison, not numeric
const nums = [10, 5, 100, 20, 1];
nums.sort();
console.log(nums); // [1, 10, 100, 20, 5] ← "100" < "20" as strings!
// Fix: pass a compare function. Negative → a first, positive → b first.
nums.sort((a, b) => a - b);
console.log(nums); // [1, 5, 10, 20, 100] ascending
nums.sort((a, b) => b - a);
console.log(nums); // [100, 20, 10, 5, 1] descending
// Sorting objects by a field
const products = [
{ name: "Laptop", price: 999 },
{ name: "Phone", price: 699 },
{ name: "Tablet", price: 399 }
];
products.sort((a, b) => a.price - b.price); // cheapest first
products.sort((a, b) => a.name.localeCompare(b.name)); // A→Z, locale-aware
For a multi-key sort, compare the primary field first and fall back to the secondary only on a tie:
const students = [
{ name: "Alice", grade: "A", score: 95 },
{ name: "Bob", grade: "B", score: 85 },
{ name: "Eve", grade: "A", score: 91 }
];
// Grade A→B, then higher score first within a grade
students.sort((a, b) =>
a.grade.localeCompare(b.grade) || b.score - a.score
);
✅ Preserve the original
Since sort() and reverse() mutate, copy first when you need the source intact: const sorted = [...arr].sort((a, b) => a - b);. Modern engines also offer arr.toSorted() and arr.toReversed(), which return a new array and never touch the original.
slice, concat & join
These accessor methods never mutate — they're the safe way to derive new data.
slice(start, end) — copy a range
const animals = ["ant", "bison", "camel", "duck", "elephant"];
console.log(animals.slice(2)); // ["camel", "duck", "elephant"]
console.log(animals.slice(2, 4)); // ["camel", "duck"] end is EXCLUSIVE
console.log(animals.slice(-2)); // ["duck", "elephant"] negative from end
const copy = animals.slice(); // shallow copy of the whole array
console.log(animals); // unchanged
concat() — merge without mutating
const a = ["a", "b"];
const b = ["c", "d"];
console.log(a.concat(b)); // ["a", "b", "c", "d"]
console.log([...a, ...b]); // ["a", "b", "c", "d"] — spread does the same
console.log(a); // ["a", "b"] original untouched
join() — array to string
const elements = ["Fire", "Air", "Water"];
console.log(elements.join()); // "Fire,Air,Water"
console.log(elements.join(" + ")); // "Fire + Air + Water"
// Build CSV from rows
const rows = [["John", 32], ["Sara", 27]];
const csv = rows.map(r => r.join(",")).join("\n");
console.log(csv); // "John,32\nSara,27"
Searching Arrays
Pick the right search tool for the job: value lookups use indexOf/includes; condition-based lookups use find/findIndex.
const beasts = ["ant", "bison", "camel", "duck", "bison"];
console.log(beasts.indexOf("bison")); // 1 (first match, or -1 if absent)
console.log(beasts.lastIndexOf("bison")); // 4 (search from the end)
console.log(beasts.includes("camel")); // true — cleaner than indexOf(...) !== -1
// includes() finds NaN; indexOf() cannot
console.log([NaN].indexOf(NaN)); // -1
console.log([NaN].includes(NaN)); // true
// find / findIndex take a callback and stop at the first match
const inventory = [
{ name: "apples", qty: 2 },
{ name: "bananas", qty: 0 },
{ name: "cherries", qty: 5 }
];
console.log(inventory.find(i => i.qty === 0)); // { name: "bananas", qty: 0 }
console.log(inventory.findIndex(i => i.qty === 0)); // 1
console.log(inventory.findLast(i => i.qty > 0)); // { name: "cherries", qty: 5 } (ES2023)
💡 Which one?
Use includes() for a yes/no "is this value present?". Use indexOf() when you need the position. Use find() when the match depends on a condition (e.g. an object with a matching id). Reaching for filter(...)[0] works but wastes effort — find() stops at the first hit.
Hands-on Exercise
🏋️ Build a Playlist Manager
Objective: Exercise the mutator and search methods by managing an ordered list of songs.
Instructions:
- Start with a class holding
this.songs = []. add(song)appends withpush();removeAt(i)deletes withsplice().move(from, to)shouldsplice()the song out andsplice()it back in at the new position.find(title)returns the index usingfindIndex()(case-insensitive).- Add a
sortedByTitle()that returns a copy in A→Z order without disturbing playback order.
💡 Hint
To move an item: const [s] = this.songs.splice(from, 1); this.songs.splice(to, 0, s);. For the non-mutating sort: [...this.songs].sort((a, b) => a.localeCompare(b)).
✅ Example solution
class Playlist {
songs = [];
add(song) { this.songs.push(song); }
removeAt(i) { return this.songs.splice(i, 1)[0]; }
move(from, to) {
const [song] = this.songs.splice(from, 1);
this.songs.splice(to, 0, song);
}
find(title) {
return this.songs.findIndex(
s => s.toLowerCase() === title.toLowerCase()
);
}
sortedByTitle() {
return [...this.songs].sort((a, b) => a.localeCompare(b));
}
}
const pl = new Playlist();
pl.add("Stronger");
pl.add("Thunderstruck");
pl.add("Eye of the Tiger");
pl.move(2, 0); // Eye of the Tiger jumps to the front
console.log(pl.songs); // ["Eye of the Tiger", "Stronger", "Thunderstruck"]
console.log(pl.find("stronger")); // 1
console.log(pl.sortedByTitle()); // A→Z copy; pl.songs stays in play order
🎯 Quick Quiz
Question 1: Which pair of methods removes and adds elements at the front of an array?
Question 2: What does [10, 5, 100].sort() return, and why?
Question 3: You need to keep an original array intact. Which method is safe to call directly on it?
Best Practices
✅ Do
- Always give
sort()a compare function when sorting numbers or objects. - Copy before mutating (
[...arr]) when the original must survive. - Prefer
includes()for presence checks andfind()for condition matches. - Capture
splice()'s return value when you care about what was removed.
⚠️ Don't
- Don't assume a method is non-mutating — check whether it's a mutator or accessor.
- Don't lean on
shift()/unshift()in hot loops over huge arrays (O(n) each). - Don't mix up
slice(copies) andsplice(mutates). - Don't use
filter(...)[0]wherefind()is clearer and faster.
Summary & Quiz
🎉 Key Takeaways
- Mutators (
push,pop,shift,unshift,splice,sort,reverse) change the original array. - Accessors (
slice,concat,join, the search methods) return new data and leave the source alone. splice()adds, removes, and replaces at any index;slice()just copies a range.sort()is string-based by default — pass a compare function for numbers and objects.- Search with
includes/indexOffor values andfind/findIndexfor conditions.
📚 Further Reading
🚀 What's Next?
Next up are the iteration methods — map, filter, and reduce — the declarative, functional tools that transform and aggregate whole arrays in a single expressive line.
🎉 Great progress!
You can now reshape any array with confidence. Let's make it functional.