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:
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 arrayThe 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.
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:
// 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.
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 arrayCombining push() and pop() can implement a Stack data structure, following the "Last In, First Out" (LIFO) principle:
// 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 undoBeginning 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.
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.
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:
// 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 queuesplice() - 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
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 remainsInsert Elements
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
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:
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
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:
// 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:
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
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.
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():
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.
// 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
// 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)
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:
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:
// ❌ 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:
// 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 slowerIn 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.