FullStack Course LogoFullStack Course
Module: JavaScript
JavaScript·053·7 MIN READ

053: Array Methods I: Mutation, Copying, and Search

TOPICS COVERED: Array Methods I: Mutation, Copying, and Search

Outcomes

By the end of this lesson, you can:

  • add/remove end elements with push() and pop();
  • add/remove beginning elements with unshift() and shift();
  • copy a range with non-mutating slice();
  • insert, replace, or remove a range with mutating splice();
  • test membership with includes() and locate values with indexOf();
  • state each method's return value; and
  • choose intentionally between mutation and a new array.

Prerequisites and Retrieval

Before working through these methods, retrieve the basics: array literals, indexes, length, mutation, and references.

  1. What is the first array index?
  2. Does const items = [] prevent items from being mutated?
  3. Why is items[items.length] not the final current element?

For today's examples, use arrays of primitive values. Object identity and shallow copies matter more once we reach the later data lessons.

Terms

Beginner Explanation and Mental Model

Array methods are functions attached to an array. When you reach for one, do not stop at remembering its name. Ask three practical questions:

  1. Does it mutate the original array?
  2. What arguments does it expect?
  3. What does it return?

The return value is a frequent source of bugs. Similar-looking methods can return a new length, a removed element, a removed-elements array, or a search result.

MethodMain actionMutates?Return value
push(a)add to endyesnew length
pop()remove from endyesremoved element or undefined
unshift(a)add to beginningyesnew length
shift()remove from beginningyesremoved element or undefined
slice(start, end)copy included rangenonew array
splice(start, count, ...items)remove/insert in placeyesarray of removed elements
includes(value)ask whether value existsnoBoolean
indexOf(value)find first matching indexnoindex or -1

End and beginning operations

js
const queue = ["A", "B"];
const lengthAfterPush = queue.push("C"); // queue is A,B,C; returns 3
const last = queue.pop();                 // queue is A,B; returns C
const lengthAfterUnshift = queue.unshift("Start"); // returns 3
const first = queue.shift();              // returns Start

All four methods mutate queue. Adding or removing at the beginning also requires existing indexes to move. That makes end operations a simpler fit when both choices express the same data model. The decision should still come from the meaning of the data, not from micro-optimization alone.

slice: end excluded

js
const letters = ["a", "b", "c", "d"];
const middle = letters.slice(1, 3); // ["b", "c"]

Index 1 is included, while index 3 is the stopping boundary and is excluded. letters is unchanged. If end is omitted, slice() copies through the end; with no arguments, it makes a shallow outer copy. Negative indexes count backward from the end.

splice: edit in place

js
const letters = ["a", "b", "c", "d"];
const removed = letters.splice(1, 2, "x", "y");
// letters: ["a", "x", "y", "d"]
// removed: ["b", "c"]

Read this call as: start at index 1, remove 2 elements, and insert the remaining arguments in their place. splice(index, 1) removes one known index. splice(index, 0, value) inserts a value without removing anything. The second argument is not an end index; it is a count of elements to remove.

Use includes() when the question is simply whether a value is present. Use indexOf() when you need the first matching position. If there is no match, indexOf() returns the sentinel value -1:

js
const roles = ["viewer", "editor"];
console.log(roles.includes("editor")); // true
console.log(roles.indexOf("editor"));  // 1
console.log(roles.indexOf("admin"));  // -1

For ordinary primitive values, both methods use essentially strict value matching. One notable exception is NaN: includes(NaN) can find it, whereas indexOf(NaN) returns -1. For beginner lists, keep searches focused on stable primitive values.

Worked Example: Task Queue

js
const tasks = ["Write code", "Run tests"];

const newLength = tasks.push("Review output");
console.log("After push:", tasks);
console.log("New length:", newLength);

tasks.unshift("Read requirements");
console.log("After unshift:", tasks);

const currentTask = tasks.shift();
console.log("Started:", currentTask);

const completedTask = tasks.pop();
console.log("Removed from end:", completedTask);
console.log("Remaining:", tasks);

console.log("Contains tests:", tasks.includes("Run tests"));
console.log("Tests index:", tasks.indexOf("Run tests"));

Expected output:

text
After push: ["Write code", "Run tests", "Review output"]
New length: 3
After unshift: ["Read requirements", "Write code", "Run tests", "Review output"]
Started: Read requirements
Removed from end: Review output
Remaining: ["Write code", "Run tests"]
Contains tests: true
Tests index: 1

The exact way a browser formats an array in the console can vary. The behavior does not: push() returns the new length, not the item that was added; shift() and pop() return the individual values they remove. The search calls do not alter the remaining array.

Intermediate Example: Copy and Edit Product Names

js
const products = ["Mouse", "Keyboard", "Monitor", "Webcam", "Headset"];

const featured = products.slice(1, 4);
console.log("Featured:", featured);
console.log("Original after slice:", products);

const removedProducts = products.splice(2, 2, "Laptop stand", "USB hub");
console.log("Removed:", removedProducts);
console.log("Original after splice:", products);

const searchName = "USB hub";
const searchIndex = products.indexOf(searchName);

if (searchIndex !== -1) {
  console.log(`${searchName} found at index ${searchIndex}.`);
} else {
  console.log(`${searchName} not found.`);
}

Expected output:

text
Featured: ["Keyboard", "Monitor", "Webcam"]
Original after slice: ["Mouse", "Keyboard", "Monitor", "Webcam", "Headset"]
Removed: ["Monitor", "Webcam"]
Original after splice: ["Mouse", "Keyboard", "Laptop stand", "USB hub", "Headset"]
USB hub found at index 3.

The guard around splice() is deliberate. Never write if (products.indexOf(searchName)): index 0 is falsy even though it means “found,” while -1 is truthy even though it means “absent.” Compare explicitly with !== -1, or use includes() if you only need the membership answer.

Optional Advanced Extension: Non-Mutating Splice Alternative

Modern stable JavaScript also provides toSpliced(). It accepts splice-like arguments but returns a new array instead of editing the original:

js
const original = ["a", "b", "c"];
const updated = original.toSpliced(1, 1, "x");

console.log(original); // ["a", "b", "c"]
console.log(updated);  // ["a", "x", "c"]

This is useful when callers expect immutable-style updates. The required method for this lesson is still splice(), so first become comfortable with its mutation and return value. Also, do not claim that slice() can directly insert replacements: it only copies ranges.

Mistakes and Debugging

  • Assigning push result as the array: items = items.push(value) makes items a number. Call items.push(value) and keep the original binding.
  • Expecting pop/shift to return an array: each returns one removed element.
  • Confusing slice and splice: slice copies; splice edits.
  • Treating slice end as included: it is excluded.
  • Treating splice delete count as end index: it is how many to remove.
  • Checking indexOf by truthiness: compare with -1.
  • Splicing at -1 after a failed search: this edits from the end. Verify the index first.
  • Expecting copying methods to deep-clone objects: standard array copy operations are shallow.
  • Ignoring mutation in a function: document mutation or return a new array so callers know what happens.

When a method behaves unexpectedly, log two things before and after the call: the array itself and the return value captured from the method. Separating those observations usually makes it clear whether the misunderstanding is about mutation, the selected range, or the return type.

Best Practices

  • Choose a method by required semantics, including mutation and return value.
  • Use includes() for yes/no membership and indexOf() when the position is needed.
  • Check indexOf() against -1 before calling splice().
  • Store removed values when they matter.
  • Use slice() for ranges and shallow outer copies.
  • Keep splice() arguments in named variables for nontrivial edits.
  • Prefer non-mutating operations when shared references or application state make mutation surprising.
  • Use const for the binding even when deliberately mutating the same array.

Tiered Exercises

Core

Start with ["CSS", "JavaScript"]. Add "HTML" to the beginning, add "Git" to the end, remove both ends while storing returned values, and log every intermediate array and return value.

Practice

Given const cities = ["Chennai", "Madurai", "Salem", "Trichy", "Coimbatore"], copy indexes 1-3 with slice, replace "Salem" and "Trichy" with "Erode" using splice, then search for "Erode" and "Salem".

Professional Extension

Write removeItem(items, item) that mutates the supplied array only if item exists. It should return the removed value, or null when absent. Test an item at index 0, a middle item, and an absent item.

Complete Solutions

js
const topics = ["CSS", "JavaScript"];
const frontLength = topics.unshift("HTML");
console.log(topics, frontLength);

const endLength = topics.push("Git");
console.log(topics, endLength);

const first = topics.shift();
console.log(topics, first);

const last = topics.pop();
console.log(topics, last);

Final array: ["CSS", "JavaScript"]; removed values: "HTML" and "Git".

js
const cities = ["Chennai", "Madurai", "Salem", "Trichy", "Coimbatore"];
const middleCities = cities.slice(1, 4);
const removedCities = cities.splice(2, 2, "Erode");

console.log(middleCities);              // Madurai, Salem, Trichy
console.log(removedCities);             // Salem, Trichy
console.log(cities);                    // Chennai, Madurai, Erode, Coimbatore
console.log(cities.includes("Erode"));  // true
console.log(cities.indexOf("Erode"));   // 2
console.log(cities.includes("Salem"));  // false
console.log(cities.indexOf("Salem"));   // -1
js
function removeItem(items, item) {
  const index = items.indexOf(item);
  if (index === -1) {
    return null;
  }
  const removedItems = items.splice(index, 1);
  return removedItems[0];
}

const values = ["a", "b", "c", "d"];
console.log(removeItem(values, "a")); // a
console.log(removeItem(values, "c")); // c
console.log(removeItem(values, "z")); // null
console.log(values); // ["b", "d"]

Checking -1 prevents an absent item from accidentally removing the last element.

Recap and Exit Questions

Array methods differ in three ways you should keep separate: what they do, whether they mutate, and what they return. The end and beginning methods mutate. slice() copies an end-exclusive range. splice() edits in place and returns removed elements. includes() answers a membership question, while indexOf() returns a position or -1.

  1. Which required methods mutate the original array?
  2. What do push() and pop() return?
  3. Why does slice(1, 3) omit index 3?
  4. What does the second splice argument mean?
  5. Why is if (items.indexOf(value)) incorrect?

Official References

References checked 2026-08-24.

Reader page: /javascript/lesson/053/array-methods-i-mutation-copying-and-search