Skip to content

Array Mutating Methods: Operations That Directly Modify the Original Array

On a construction site, workers have two ways to adjust material arrangements. One is to rearrange materials on site—moving bricks, reordering, adding or removing materials, which changes the actual state of the site. Another is to make a copy first, modify the copy while keeping the original arrangement intact. In JavaScript, array methods have similar classifications. Mutating Methods are like the first approach—they directly modify the original array, changing the array's content, order, or length. Understanding the behavior of these methods is crucial for avoiding unexpected side effects.

What are Mutating Methods

Mutating methods are methods that directly modify the original array they are called on. After calling these methods, the original array's content, order, or length changes. This contrasts with non-mutating methods, which return a new array without modifying the original.

Let's start with a simple comparison:

javascript
let numbers = [1, 2, 3];

// Mutating method: modifies original array
numbers.push(4);
console.log(numbers); // [1, 2, 3, 4] - original array modified

let fruits = ["apple", "banana"];

// Non-mutating method: returns new array
let moreFruits = fruits.concat(["orange"]);
console.log(fruits); // ["apple", "banana"] - original array unchanged
console.log(moreFruits); // ["apple", "banana", "orange"] - new array

The characteristics of mutating methods are direct, efficient but with side effects. They don't need to create new arrays, saving memory, but it also means you need to be careful to avoid accidentally modifying shared array data.

End Operations: push and pop

push() - Add Elements to the End

The push() method adds one or more elements to the end of an array and returns the new array length. This is one of the most commonly used array operations.

javascript
let colors = ["red", "green"];

// Add single element
let newLength = colors.push("blue");
console.log(colors); // ["red", "green", "blue"]
console.log(newLength); // 3 - returns new length

// Add multiple elements at once
colors.push("yellow", "purple", "orange");
console.log(colors); // ["red", "green", "blue", "yellow", "purple", "orange"]

// push can add any type of value
let mixed = [1, 2];
mixed.push("three", { value: 4 }, [5, 6]);
console.log(mixed); // [1, 2, "three", { value: 4 }, [5, 6]]

The time complexity of push() is O(1) because it just adds elements to the end of the array without moving other elements. This makes it very efficient, especially suitable for building lists or collecting data.

Practical Application Scenarios:

javascript
// Collect user input
let searchHistory = [];

function recordSearch(query) {
  searchHistory.push({
    query: query,
    timestamp: new Date(),
  });
}

recordSearch("JavaScript arrays");
recordSearch("Array methods");
recordSearch("push vs concat");

console.log(searchHistory);
// [
//   { query: "JavaScript arrays", timestamp: ... },
//   { query: "Array methods", timestamp: ... },
//   { query: "push vs concat", timestamp: ... }
// ]

pop() - Remove Elements from the End

The pop() method removes the last element of an array and returns the removed element. If the array is empty, it returns undefined.

javascript
let stack = ["first", "second", "third"];

// Remove last element
let removed = stack.pop();
console.log(removed); // "third"
console.log(stack); // ["first", "second"]

// Continue removing
stack.pop(); // "second"
stack.pop(); // "first"
console.log(stack); // []

// Calling pop on empty array
let empty = stack.pop();
console.log(empty); // undefined
console.log(stack); // [] - still empty array

Combining push() and pop() can implement a Stack data structure, following the "Last In, First Out" (LIFO) principle:

javascript
// Implement simple undo functionality
let actions = [];

function doAction(action) {
  actions.push(action);
  console.log(`Performed: ${action}`);
}

function undo() {
  let lastAction = actions.pop();
  if (lastAction) {
    console.log(`Undoing: ${lastAction}`);
  } else {
    console.log("Nothing to undo");
  }
}

doAction("Create document"); // Performed: Create document
doAction("Add text"); // Performed: Add text
doAction("Format text"); // Performed: Format text

undo(); // Undoing: Format text
undo(); // Undoing: Add text
undo(); // Undoing: Create document
undo(); // Nothing to undo

Beginning Operations: unshift and shift

unshift() - Add Elements to the Beginning

The unshift() method adds one or more elements to the beginning of an array and returns the new array length. Unlike push(), it adds elements at index 0 position.

javascript
let numbers = [3, 4, 5];

// Add single element to beginning
numbers.unshift(2);
console.log(numbers); // [2, 3, 4, 5]

// Add multiple elements at once
numbers.unshift(-1, 0, 1);
console.log(numbers); // [-1, 0, 1, 2, 3, 4, 5]

Note that unshift() performs worse than push() because it needs to move all existing elements one position back. For large arrays, this operation can be relatively slow (time complexity O(n)).

shift() - Remove Elements from the Beginning

The shift() method removes the first element of an array and returns the removed element. Similarly, it performs worse than pop() because it needs to move all remaining elements.

javascript
let queue = ["Alice", "Bob", "Charlie", "David"];

// Remove first element
let first = queue.shift();
console.log(first); // "Alice"
console.log(queue); // ["Bob", "Charlie", "David"]

first = queue.shift();
console.log(first); // "Bob"
console.log(queue); // ["Charlie", "David"]

Combining push() and shift() can implement a Queue data structure, following the "First In, First Out" (FIFO) principle:

javascript
// Simulate queuing system
let customerQueue = [];

function joinQueue(customerName) {
  customerQueue.push(customerName);
  console.log(`${customerName} joined the queue`);
  console.log(`Queue: [${customerQueue.join(", ")}]`);
}

function serveCustomer() {
  let customer = customerQueue.shift();
  if (customer) {
    console.log(`Serving ${customer}`);
  } else {
    console.log("No customers in queue");
  }
  console.log(`Queue: [${customerQueue.join(", ")}]`);
}

joinQueue("Alice"); // Alice joined the queue
joinQueue("Bob"); // Bob joined the queue
joinQueue("Charlie"); // Charlie joined the queue

serveCustomer(); // Serving Alice
serveCustomer(); // Serving Bob
joinQueue("David"); // David joined the queue
serveCustomer(); // Serving Charlie
serveCustomer(); // Serving David
serveCustomer(); // No customers in queue

splice() - The Swiss Army Knife of Methods

splice() is one of the most powerful and flexible mutating methods for arrays. It can delete, insert, or replace elements in an array. This method accepts multiple parameters:

  • start: Index position to start modification
  • deleteCount: Number of elements to delete (optional)
  • items: New elements to insert (optional, can be multiple)

Delete Elements

javascript
let fruits = ["apple", "banana", "orange", "grape", "mango"];

// Delete 2 elements starting from index 2
let removed = fruits.splice(2, 2);
console.log(removed); // ["orange", "grape"] - returns deleted elements
console.log(fruits); // ["apple", "banana", "mango"]

// Delete all subsequent elements starting from index 1
let numbers = [1, 2, 3, 4, 5];
numbers.splice(1);
console.log(numbers); // [1] - only first element remains

Insert Elements

javascript
let colors = ["red", "yellow"];

// Insert element at index 1, delete nothing (deleteCount is 0)
colors.splice(1, 0, "orange");
console.log(colors); // ["red", "orange", "yellow"]

// Insert multiple elements at once
colors.splice(2, 0, "green", "blue");
console.log(colors); // ["red", "orange", "green", "blue", "yellow"]

Replace Elements

javascript
let items = ["book", "pen", "eraser", "ruler"];

// Starting from index 1, delete 2 elements, insert 2 new elements
items.splice(1, 2, "notebook", "pencil");
console.log(items); // ["book", "notebook", "pencil", "ruler"]

// Delete more than insert: shorten array
let nums = [1, 2, 3, 4, 5];
nums.splice(1, 3, 99);
console.log(nums); // [1, 99, 5]

// Insert more than delete: lengthen array
let letters = ["a", "b", "e"];
letters.splice(2, 0, "c", "d");
console.log(letters); // ["a", "b", "c", "d", "e"]

Negative Indices

splice() supports negative indices, counting from the end of the array:

javascript
let data = [1, 2, 3, 4, 5];

// -2 means the second-to-last position
data.splice(-2, 1);
console.log(data); // [1, 2, 3, 5] - deleted 4

// Insert before the last position
data.splice(-1, 0, 99);
console.log(data); // [1, 2, 3, 99, 5]

Practical Application: Managing Playlist

javascript
let playlist = ["Song A", "Song B", "Song C", "Song D", "Song E"];

// Remove a song
function removeSong(index) {
  let removed = playlist.splice(index, 1);
  console.log(`Removed: ${removed[0]}`);
}

// Insert song at specific position
function insertSong(index, songName) {
  playlist.splice(index, 0, songName);
  console.log(`Inserted ${songName} at position ${index}`);
}

// Replace song
function replaceSong(index, newSong) {
  let old = playlist.splice(index, 1, newSong);
  console.log(`Replaced ${old[0]} with ${newSong}`);
}

insertSong(2, "New Hit"); // Inserted New Hit at position 2
console.log(playlist); // ["Song A", "Song B", "New Hit", "Song C", "Song D", "Song E"]

replaceSong(0, "Latest Song"); // Replaced Song A with Latest Song
console.log(playlist); // ["Latest Song", "Song B", "New Hit", "Song C", "Song D", "Song E"]

removeSong(3); // Removed: Song C
console.log(playlist); // ["Latest Song", "Song B", "New Hit", "Song D", "Song E"]

sort() - Sort Arrays

The sort() method sorts array elements, directly modifying the original array and returning the sorted array.

Default Sorting Behavior

By default, sort() converts all elements to strings and sorts them according to Unicode code point order:

javascript
// String sorting works normally
let fruits = ["banana", "apple", "orange", "grape"];
fruits.sort();
console.log(fruits); // ["apple", "banana", "grape", "orange"]

// ⚠️ Number sorting might have unexpected results
let numbers = [10, 5, 40, 25, 1000, 1];
numbers.sort();
console.log(numbers); // [1, 10, 1000, 25, 40, 5] - sorted as strings!

Why does [10, 5, 40] become [10, 40, 5] after sorting? Because in string comparison, "10" is less than "5" (since "1" is less than "5").

Using Comparison Functions

To correctly sort numbers or implement custom sorting logic, you need to provide a comparison function:

javascript
let numbers = [10, 5, 40, 25, 1000, 1];

// Ascending sort
numbers.sort((a, b) => a - b);
console.log(numbers); // [1, 5, 10, 25, 40, 1000]

// Descending sort
numbers.sort((a, b) => b - a);
console.log(numbers); // [1000, 40, 25, 10, 5, 1]

How comparison functions work:

  • If return value < 0, a comes before b
  • If return value > 0, b comes before a
  • If return value = 0, relative order of a and b remains unchanged

Complex Object Sorting

javascript
let students = [
  { name: "Alice", grade: 85 },
  { name: "Bob", grade: 92 },
  { name: "Charlie", grade: 78 },
  { name: "David", grade: 92 },
  { name: "Emma", grade: 88 },
];

// Sort by grade in ascending order
students.sort((a, b) => a.grade - b.grade);
console.log(students);
// [
//   { name: "Charlie", grade: 78 },
//   { name: "Alice", grade: 85 },
//   { name: "Emma", grade: 88 },
//   { name: "Bob", grade: 92 },
//   { name: "David", grade: 92 }
// ]

// Sort by name alphabetically
students.sort((a, b) => a.name.localeCompare(b.name));
console.log(students);
// [
//   { name: "Alice", grade: 85 },
//   { name: "Bob", grade: 92 },
//   { name: "Charlie", grade: 78 },
//   { name: "David", grade: 92 },
//   { name: "Emma", grade: 88 }
// ]

// Multi-level sort: first by grade descending, then by name ascending for same grades
students.sort((a, b) => {
  if (b.grade !== a.grade) {
    return b.grade - a.grade; // Grade descending
  }
  return a.name.localeCompare(b.name); // Name ascending
});
console.log(students);
// [
//   { name: "Bob", grade: 92 },
//   { name: "David", grade: 92 },
//   { name: "Emma", grade: 88 },
//   { name: "Alice", grade: 85 },
//   { name: "Charlie", grade: 78 }
// ]

reverse() - Reverse Array

The reverse() method reverses the order of array elements, making the first element the last and the last element the first.

javascript
let numbers = [1, 2, 3, 4, 5];
numbers.reverse();
console.log(numbers); // [5, 4, 3, 2, 1]

let words = ["hello", "world", "JavaScript"];
words.reverse();
console.log(words); // ["JavaScript", "world", "hello"]

reverse() is often used in combination with sort():

javascript
let scores = [85, 92, 78, 95, 88];

// Sort ascending first, then reverse to get descending
scores.sort((a, b) => a - b).reverse();
console.log(scores); // [95, 92, 88, 85, 78]

// More efficient way: sort descending directly
scores.sort((a, b) => b - a);
console.log(scores); // [95, 92, 88, 85, 78]

fill() - Fill Array

The fill() method fills all or part of an array with a fixed value.

javascript
// Create and fill new array
let zeros = new Array(5).fill(0);
console.log(zeros); // [0, 0, 0, 0, 0]

// Fill part of array (startIndex, endIndex)
let numbers = [1, 2, 3, 4, 5];
numbers.fill(99, 1, 4); // From index 1 to 3 (not including 4)
console.log(numbers); // [1, 99, 99, 99, 5]

// Fill from certain position to end
let letters = ["a", "b", "c", "d", "e"];
letters.fill("x", 2);
console.log(letters); // ["a", "b", "x", "x", "x"]

Practical Application: Initialize Game Board

javascript
// Create an 8x8 board initialized to empty
function createBoard(size) {
  return Array.from({ length: size }, () => new Array(size).fill(null));
}

let chessBoard = createBoard(8);
console.log(chessBoard[0]); // [null, null, null, null, null, null, null, null]

// Initialize specific area
function initializeRow(row, value) {
  row.fill(value);
}

initializeRow(chessBoard[0], "♜"); // Initialize first row
console.log(chessBoard[0]); // ["♜", "♜", "♜", "♜", "♜", "♜", "♜", "♜"]

copyWithin() - Internal Copy

The copyWithin() method copies a segment of elements to another position within the array, overwriting existing elements.

Syntax: array.copyWithin(target, start, end)

  • target: Target position to copy to
  • start: Starting copy position (optional, default 0)
  • end: End copy position (optional, default array length)
javascript
let numbers = [1, 2, 3, 4, 5];

// Copy elements from indices 0-2 to index 3 position
numbers.copyWithin(3, 0, 2);
console.log(numbers); // [1, 2, 3, 1, 2]

// Copy last 3 elements to beginning
let data = [1, 2, 3, 4, 5, 6];
data.copyWithin(0, 3, 6);
console.log(data); // [4, 5, 6, 4, 5, 6]

// Use negative indices
let items = ["a", "b", "c", "d", "e"];
items.copyWithin(-2, 0, 2);
console.log(items); // ["a", "b", "c", "a", "b"]

This method is relatively uncommon but useful when you need to move or copy data within an array, such as implementing circular buffers.

Important Considerations for Mutating Methods

1. Side Effect Issues

Mutating methods modify the original array, which can lead to unexpected side effects, especially when arrays are referenced in multiple places:

javascript
let originalList = ["apple", "banana", "orange"];
let sharedList = originalList; // Shared reference

originalList.push("grape");
console.log(sharedList); // ["apple", "banana", "orange", "grape"]
// sharedList also modified!

// If you need to keep original array unchanged, copy first
let safeList = [...originalList];
safeList.push("mango");
console.log(originalList); // ["apple", "banana", "orange", "grape"] - unchanged
console.log(safeList); // ["apple", "banana", "orange", "grape", "mango"]

2. Functional Programming Considerations

In functional programming paradigms, pure functions should not produce side effects. If you pursue pure function style, you should avoid using mutating methods:

javascript
// ❌ Impure function: modifies input array
function addItemImpure(array, item) {
  array.push(item);
  return array;
}

let items = [1, 2, 3];
let result = addItemImpure(items, 4);
console.log(items); // [1, 2, 3, 4] - original array modified

// ✅ Pure function: returns new array
function addItemPure(array, item) {
  return [...array, item];
}

let numbers = [1, 2, 3];
let newNumbers = addItemPure(numbers, 4);
console.log(numbers); // [1, 2, 3] - original array unchanged
console.log(newNumbers); // [1, 2, 3, 4]

3. Performance Trade-offs

Mutating methods are generally more efficient than non-mutating methods because they don't need to create new arrays:

javascript
// Mutating method: fast, but modifies original array
let largeArray = new Array(100000).fill(0);
console.time("mutating");
largeArray.push(1);
console.timeEnd("mutating"); // Very fast

// Non-mutating method: needs to copy entire array
let anotherLargeArray = new Array(100000).fill(0);
console.time("non-mutating");
let newArray = [...anotherLargeArray, 1];
console.timeEnd("non-mutating"); // Relatively slower

In performance-critical scenarios (like processing large arrays or frequent operations), mutating methods might be the better choice. But in most cases, code maintainability and predictability are more important.

Summary

Mutating methods are an important part of JavaScript array operations. They directly modify the original array, providing efficient data manipulation methods:

  • push() / pop() - Add/remove at end, implement stack structure (LIFO)
  • unshift() / shift() - Add/remove at beginning, with push implements queue (FIFO)
  • splice() - Universal method, can delete, insert, replace elements at any position
  • sort() - Sort array, needs comparison function for numbers and objects
  • reverse() - Reverse array order
  • fill() - Fill array with fixed value
  • copyWithin() - Copy elements within array

Remember when using mutating methods:

  • They modify the original array and can produce side effects
  • When you need to keep the original array unchanged, create a copy first
  • In functional programming, consider using non-mutating alternatives
  • In performance-critical scenarios, mutating methods are usually more efficient

In the next article, we'll explore non-mutating methods to learn how to process data without modifying the original array.