Skip to main content

🛠️ 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/pop for stack behavior and shift/unshift for 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() and concat()
  • Search arrays with indexOf, includes, find, and findIndex
  • 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.

graph TD A[Array methods] --> B[Mutators
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.

A stack showing push and pop at the top Four stacked boxes; push adds a new box on top and pop removes the top box. Item 1 (bottom) Item 2 Item 3 Item 4 (top) push() pop()
Figure 1 — In a stack, both 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"

Hands-on Exercise

🏋️ Build a Playlist Manager

Objective: Exercise the mutator and search methods by managing an ordered list of songs.

Instructions:

  1. Start with a class holding this.songs = [].
  2. add(song) appends with push(); removeAt(i) deletes with splice().
  3. move(from, to) should splice() the song out and splice() it back in at the new position.
  4. find(title) returns the index using findIndex() (case-insensitive).
  5. 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 and find() 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) and splice (mutates).
  • Don't use filter(...)[0] where find() 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/indexOf for values and find/findIndex for conditions.

📚 Further Reading

🚀 What's Next?

Next up are the iteration methodsmap, 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.