We are still cooking the magic in the way!
Array Methods: push, pop, shift, unshift, splice
Understanding Mutating vs Non-Mutating Methods
Before diving into specific array methods, it is critical to understand the difference between mutating and non-mutating methods. A mutating method (also called an "in-place" method) changes the original array directly. After you call a mutating method, the array you started with is permanently altered. A non-mutating method creates and returns a new array without touching the original. The original remains exactly as it was.
This distinction matters enormously in real-world applications. In modern JavaScript frameworks like React, Vue, and Angular, directly mutating state can cause bugs, missed re-renders, and hard-to-track issues. Many developers prefer non-mutating methods because they make code more predictable -- you always know that your original data is safe. However, mutating methods are perfectly fine when you own the data and performance matters, such as inside a local function that creates and modifies its own array.
In this lesson, we will cover the most important mutating methods first, then move to non-mutating alternatives. By the end, you will understand exactly when to use each approach and how to convert between them.
Example: Mutation vs Immutability at a Glance
// MUTATING: the original array is changed
const fruits = ['apple', 'banana'];
fruits.push('cherry');
console.log(fruits); // ['apple', 'banana', 'cherry'] -- original changed!
// NON-MUTATING: a new array is returned, original is untouched
const numbers = [1, 2, 3];
const doubled = numbers.map(n => n * 2);
console.log(numbers); // [1, 2, 3] -- original unchanged
console.log(doubled); // [2, 4, 6] -- new array
// A common pattern: creating a new array instead of mutating
const original = ['a', 'b', 'c'];
const withD = [...original, 'd']; // non-mutating add
console.log(original); // ['a', 'b', 'c'] -- safe
console.log(withD); // ['a', 'b', 'c', 'd']
push() -- Adding Elements to the End
The push() method adds one or more elements to the end of an array and returns the new length of the array. This is the most commonly used method for adding items to an array. It is a mutating method -- it modifies the original array in place.
Example: Using push()
const colors = ['red', 'green'];
// Add one element
const newLength = colors.push('blue');
console.log(colors); // ['red', 'green', 'blue']
console.log(newLength); // 3 (push returns the new length)
// Add multiple elements at once
colors.push('yellow', 'purple', 'orange');
console.log(colors);
// ['red', 'green', 'blue', 'yellow', 'purple', 'orange']
// Push elements from another array using spread
const moreColors = ['pink', 'cyan'];
colors.push(...moreColors);
console.log(colors);
// ['red', 'green', 'blue', 'yellow', 'purple', 'orange', 'pink', 'cyan']
// Push returns the new length, not the array
const items = [];
console.log(items.push('first')); // 1
console.log(items.push('second')); // 2
console.log(items.push('third')); // 3
console.log(items); // ['first', 'second', 'third']
push() returns the new length of the array, not the array itself. This is a common source of confusion. If you need the array, reference it directly rather than using the return value of push().pop() -- Removing Elements from the End
The pop() method removes the last element from an array and returns that removed element. If the array is empty, pop() returns undefined. Like push(), it is a mutating method. Together, push() and pop() allow you to use an array as a stack (Last-In, First-Out / LIFO) data structure.
Example: Using pop()
const stack = ['page1', 'page2', 'page3', 'page4'];
// Remove and get the last element
const lastPage = stack.pop();
console.log(lastPage); // 'page4'
console.log(stack); // ['page1', 'page2', 'page3']
// Pop again
const anotherPage = stack.pop();
console.log(anotherPage); // 'page3'
console.log(stack); // ['page1', 'page2']
// Pop from an empty array
const emptyArr = [];
const result = emptyArr.pop();
console.log(result); // undefined
console.log(emptyArr); // []
// Using push and pop as a stack (LIFO)
const undoStack = [];
undoStack.push('typed A');
undoStack.push('typed B');
undoStack.push('typed C');
console.log(undoStack); // ['typed A', 'typed B', 'typed C']
// Undo actions in reverse order
console.log(undoStack.pop()); // 'typed C' (last action undone first)
console.log(undoStack.pop()); // 'typed B'
console.log(undoStack); // ['typed A']
unshift() -- Adding Elements to the Beginning
The unshift() method adds one or more elements to the beginning of an array and returns the new length. All existing elements are shifted to higher indices to make room. This is a mutating method. Be aware that unshift() is generally slower than push() because every existing element must be re-indexed.
Example: Using unshift()
const queue = ['second', 'third'];
// Add one element to the beginning
const newLen = queue.unshift('first');
console.log(queue); // ['first', 'second', 'third']
console.log(newLen); // 3
// Add multiple elements to the beginning
queue.unshift('zero-a', 'zero-b');
console.log(queue);
// ['zero-a', 'zero-b', 'first', 'second', 'third']
// The order of multiple arguments is preserved
const nums = [4, 5];
nums.unshift(1, 2, 3);
console.log(nums); // [1, 2, 3, 4, 5]
// Note: 1, 2, 3 are inserted as a group, not one at a time
// Compare: inserting one at a time reverses the order
const nums2 = [4, 5];
nums2.unshift(3);
nums2.unshift(2);
nums2.unshift(1);
console.log(nums2); // [1, 2, 3, 4, 5] -- same result here, but timing differs
unshift() has O(n) time complexity because every existing element must be shifted to a new index. For large arrays with thousands of elements, frequent unshift() calls can cause noticeable performance issues. If you need to frequently add to the beginning, consider using a different data structure or reversing your logic to use push() instead.shift() -- Removing Elements from the Beginning
The shift() method removes the first element from an array and returns that removed element. All remaining elements are shifted down to lower indices. Like unshift(), it has O(n) complexity. Together, push() and shift() allow you to use an array as a queue (First-In, First-Out / FIFO) data structure.
Example: Using shift()
const tasks = ['email', 'meeting', 'code review', 'deploy'];
// Remove and get the first element
const firstTask = tasks.shift();
console.log(firstTask); // 'email'
console.log(tasks); // ['meeting', 'code review', 'deploy']
// Shift again
const nextTask = tasks.shift();
console.log(nextTask); // 'meeting'
console.log(tasks); // ['code review', 'deploy']
// Shift from an empty array
const empty = [];
console.log(empty.shift()); // undefined
// Using push and shift as a queue (FIFO)
const printQueue = [];
printQueue.push('Document A');
printQueue.push('Document B');
printQueue.push('Document C');
console.log(printQueue); // ['Document A', 'Document B', 'Document C']
// Process in order (first in, first out)
console.log(printQueue.shift()); // 'Document A' (first added, first processed)
console.log(printQueue.shift()); // 'Document B'
console.log(printQueue); // ['Document C']
push/pop work at the end of the array, unshift/shift work at the beginning. Methods that add elements (push, unshift) return the new length. Methods that remove elements (pop, shift) return the removed element.splice() -- The Swiss Army Knife
The splice() method is the most powerful and versatile mutating array method. It can add, remove, and replace elements at any position in the array, all in a single call. Its syntax is: array.splice(startIndex, deleteCount, ...itemsToInsert). It returns an array of the removed elements (which is empty if nothing was removed).
Removing Elements with splice()
Example: Removing Elements
const months = ['Jan', 'Feb', 'March', 'Apr', 'May', 'Jun'];
// Remove 1 element at index 2
const removed = months.splice(2, 1);
console.log(removed); // ['March']
console.log(months); // ['Jan', 'Feb', 'Apr', 'May', 'Jun']
// Remove 2 elements starting at index 3
const removedTwo = months.splice(3, 2);
console.log(removedTwo); // ['May', 'Jun']
console.log(months); // ['Jan', 'Feb', 'Apr']
// Remove everything from index 1 onward (omit deleteCount)
const letters = ['a', 'b', 'c', 'd', 'e'];
const removedAll = letters.splice(1);
console.log(removedAll); // ['b', 'c', 'd', 'e']
console.log(letters); // ['a']
// Remove nothing (deleteCount = 0)
const items = ['x', 'y', 'z'];
const removedNone = items.splice(1, 0);
console.log(removedNone); // [] (empty array -- nothing removed)
console.log(items); // ['x', 'y', 'z'] (unchanged)
// Using negative index (counts from end)
const data = [10, 20, 30, 40, 50];
data.splice(-2, 1); // Remove 1 element starting from second-to-last
console.log(data); // [10, 20, 30, 50]
Adding Elements with splice()
Example: Inserting Elements
const languages = ['JavaScript', 'Python', 'Java'];
// Insert 'TypeScript' at index 1 (delete 0 elements)
languages.splice(1, 0, 'TypeScript');
console.log(languages);
// ['JavaScript', 'TypeScript', 'Python', 'Java']
// Insert multiple elements at index 3
languages.splice(3, 0, 'Go', 'Rust');
console.log(languages);
// ['JavaScript', 'TypeScript', 'Python', 'Go', 'Rust', 'Java']
// Insert at the beginning (index 0)
languages.splice(0, 0, 'HTML');
console.log(languages);
// ['HTML', 'JavaScript', 'TypeScript', 'Python', 'Go', 'Rust', 'Java']
// Insert at the end (using length as index)
languages.splice(languages.length, 0, 'C++');
console.log(languages);
// ['HTML', 'JavaScript', 'TypeScript', 'Python', 'Go', 'Rust', 'Java', 'C++']
Replacing Elements with splice()
Example: Replacing Elements
const team = ['Alice', 'Bob', 'Charlie', 'Diana', 'Eve'];
// Replace 1 element at index 2
const replaced = team.splice(2, 1, 'Carlos');
console.log(replaced); // ['Charlie'] (the removed element)
console.log(team); // ['Alice', 'Bob', 'Carlos', 'Diana', 'Eve']
// Replace 2 elements with 3 new ones (array grows)
team.splice(1, 2, 'Bilal', 'Carla', 'Carmen');
console.log(team);
// ['Alice', 'Bilal', 'Carla', 'Carmen', 'Diana', 'Eve']
// Replace 3 elements with 1 (array shrinks)
team.splice(1, 3, 'Brian');
console.log(team); // ['Alice', 'Brian', 'Diana', 'Eve']
// Replace the last element
team.splice(-1, 1, 'Emily');
console.log(team); // ['Alice', 'Brian', 'Diana', 'Emily']
splice() method always returns an array of removed elements, even if only one element was removed. If no elements were removed, it returns an empty array []. This makes it safe to always use the return value without checking.reverse() -- Reversing Array Order
The reverse() method reverses the order of elements in an array in place and returns the reversed array (the same reference). This is a mutating method. If you need a reversed copy without modifying the original, use toReversed() (ES2023) or spread and then reverse.
Example: Using reverse()
const sequence = [1, 2, 3, 4, 5];
// Reverse in place
const reversed = sequence.reverse();
console.log(sequence); // [5, 4, 3, 2, 1]
console.log(reversed); // [5, 4, 3, 2, 1]
console.log(reversed === sequence); // true (same reference!)
// Reverse a string (using array conversion)
const str = 'Hello World';
const reversedStr = [...str].reverse().join('');
console.log(reversedStr); // 'dlroW olleH'
// Non-mutating reverse using spread
const original = ['a', 'b', 'c', 'd'];
const reversedCopy = [...original].reverse();
console.log(original); // ['a', 'b', 'c', 'd'] -- unchanged
console.log(reversedCopy); // ['d', 'c', 'b', 'a']
// ES2023: toReversed() (non-mutating)
const nums = [10, 20, 30, 40];
const numsReversed = nums.toReversed();
console.log(nums); // [10, 20, 30, 40] -- unchanged
console.log(numsReversed); // [40, 30, 20, 10]
sort() -- Sorting Array Elements
The sort() method sorts the elements of an array in place and returns the sorted array. By default, sort() converts elements to strings and sorts them in lexicographic (alphabetical/Unicode) order. This works fine for strings but produces surprising results with numbers. To sort correctly, you must provide a compare function.
Example: Default Sort Behavior
// Sorting strings works as expected
const fruits = ['banana', 'apple', 'cherry', 'date'];
fruits.sort();
console.log(fruits); // ['apple', 'banana', 'cherry', 'date']
// Sorting numbers WITHOUT a compare function -- WRONG!
const numbers = [10, 5, 100, 25, 1];
numbers.sort();
console.log(numbers); // [1, 10, 100, 25, 5] -- INCORRECT!
// Numbers are converted to strings: '1' < '10' < '100' < '25' < '5'
// Upper and lowercase sort differently
const mixed = ['banana', 'Apple', 'cherry', 'avocado'];
mixed.sort();
console.log(mixed); // ['Apple', 'avocado', 'banana', 'cherry']
// Uppercase letters come before lowercase in Unicode
sort() without a compare function when sorting numbers. The default string-based sorting will give you wrong results. Always pass a compare function: arr.sort((a, b) => a - b) for ascending, arr.sort((a, b) => b - a) for descending.Example: Sort with Compare Functions
// Ascending numeric sort
const scores = [85, 92, 78, 100, 65, 88];
scores.sort((a, b) => a - b);
console.log(scores); // [65, 78, 85, 88, 92, 100]
// Descending numeric sort
scores.sort((a, b) => b - a);
console.log(scores); // [100, 92, 88, 85, 78, 65]
// How the compare function works:
// If (a - b) < 0: a comes before b
// If (a - b) > 0: b comes before a
// If (a - b) === 0: order unchanged
// Case-insensitive string sort
const names = ['charlie', 'Alice', 'bob', 'Diana'];
names.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
console.log(names); // ['Alice', 'bob', 'charlie', 'Diana']
// Sort objects by a property
const students = [
{ name: 'Ali', grade: 88 },
{ name: 'Sara', grade: 95 },
{ name: 'Omar', grade: 72 },
{ name: 'Layla', grade: 91 }
];
// Sort by grade (ascending)
students.sort((a, b) => a.grade - b.grade);
console.log(students.map(s => `${s.name}: ${s.grade}`));
// ['Omar: 72', 'Ali: 88', 'Layla: 91', 'Sara: 95']
// Sort by name (alphabetical)
students.sort((a, b) => a.name.localeCompare(b.name));
console.log(students.map(s => s.name));
// ['Ali', 'Layla', 'Omar', 'Sara']
// Multi-criteria sort: by grade descending, then by name ascending
const classmates = [
{ name: 'Ali', grade: 88 },
{ name: 'Sara', grade: 88 },
{ name: 'Omar', grade: 95 },
{ name: 'Layla', grade: 88 }
];
classmates.sort((a, b) => {
if (b.grade !== a.grade) return b.grade - a.grade;
return a.name.localeCompare(b.name);
});
console.log(classmates.map(s => `${s.name}: ${s.grade}`));
// ['Omar: 95', 'Ali: 88', 'Layla: 88', 'Sara: 88']
toSorted(), a non-mutating version of sort(). Use const sorted = arr.toSorted((a, b) => a - b) to get a sorted copy without modifying the original array.fill() -- Filling Array Elements
The fill() method fills all (or a portion of) the elements in an array with a static value. Its syntax is array.fill(value, startIndex, endIndex). The start index is inclusive and the end index is exclusive. This is a mutating method useful for initializing arrays or resetting sections of data.
Example: Using fill()
// Fill an entire array
const zeros = new Array(5).fill(0);
console.log(zeros); // [0, 0, 0, 0, 0]
// Fill with a specific value
const stars = new Array(3).fill('*');
console.log(stars); // ['*', '*', '*']
// Partial fill (start at index 2, end before index 4)
const arr = [1, 2, 3, 4, 5];
arr.fill(0, 2, 4);
console.log(arr); // [1, 2, 0, 0, 5]
// Fill from an index to the end
const data = [1, 2, 3, 4, 5];
data.fill(99, 3);
console.log(data); // [1, 2, 3, 99, 99]
// Reset all scores to zero
const scores = [85, 92, 78, 100, 65];
scores.fill(0);
console.log(scores); // [0, 0, 0, 0, 0]
// Create a grid row filled with default values
const row = new Array(8).fill('.');
console.log(row); // ['.', '.', '.', '.', '.', '.', '.', '.']
fill() uses the same reference for every slot. Modifying one will modify all. Use Array.from({ length: n }, () => ({})) instead to create distinct objects.Example: The fill() Object Reference Trap
// WRONG: all slots share the same object reference
const grid = new Array(3).fill([]);
grid[0].push('X');
console.log(grid); // [['X'], ['X'], ['X']] -- all changed!
// RIGHT: each slot gets its own array
const correctGrid = Array.from({ length: 3 }, () => []);
correctGrid[0].push('X');
console.log(correctGrid); // [['X'], [], []] -- only first changed
copyWithin() -- Copying Elements Inside the Array
The copyWithin() method copies a sequence of elements within the array to another position, overwriting existing values. Its syntax is array.copyWithin(target, start, end). The array length is not changed. This is a mutating method that is less commonly used but powerful for specific scenarios like buffer manipulation.
Example: Using copyWithin()
// Copy elements from index 3 to position 0
const arr = [1, 2, 3, 4, 5];
arr.copyWithin(0, 3);
console.log(arr); // [4, 5, 3, 4, 5]
// Elements at index 3 and 4 (which are 4 and 5) were copied to index 0 and 1
// Copy elements from index 1 to index 3 (2 elements: from 1 to before 3)
const data = ['a', 'b', 'c', 'd', 'e'];
data.copyWithin(3, 1, 3);
console.log(data); // ['a', 'b', 'c', 'b', 'c']
// Using negative indices
const nums = [1, 2, 3, 4, 5];
nums.copyWithin(-2, 0, 2); // Copy first 2 elements to the last 2 positions
console.log(nums); // [1, 2, 3, 1, 2]
// Practical: shift elements left to "delete" an element
const list = [10, 20, 30, 40, 50];
list.copyWithin(1, 2); // Copy from index 2 onward to index 1
list.length = list.length - 1; // Remove the duplicate last element
console.log(list); // [10, 30, 40, 50]
concat() -- Combining Arrays (Non-Mutating)
The concat() method merges two or more arrays into a new array without changing the existing arrays. It returns a new array containing elements from the original array followed by elements from each argument. If an argument is an array, its elements are added individually (one level of flattening). If it is not an array, it is added as-is.
Example: Using concat()
const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
const arr3 = [7, 8, 9];
// Concatenate two arrays
const combined = arr1.concat(arr2);
console.log(combined); // [1, 2, 3, 4, 5, 6]
console.log(arr1); // [1, 2, 3] -- unchanged
console.log(arr2); // [4, 5, 6] -- unchanged
// Concatenate multiple arrays
const all = arr1.concat(arr2, arr3);
console.log(all); // [1, 2, 3, 4, 5, 6, 7, 8, 9]
// Concatenate with individual values
const withExtras = arr1.concat('a', 'b', arr2);
console.log(withExtras); // [1, 2, 3, 'a', 'b', 4, 5, 6]
// Only one level of flattening
const nested = [1, 2].concat([3, [4, 5]]);
console.log(nested); // [1, 2, 3, [4, 5]] -- inner array NOT flattened
// concat vs spread (both are non-mutating)
const usingConcat = arr1.concat(arr2);
const usingSpread = [...arr1, ...arr2];
console.log(usingConcat); // [1, 2, 3, 4, 5, 6]
console.log(usingSpread); // [1, 2, 3, 4, 5, 6]
// Both produce the same result; spread is more commonly used in modern code
slice() -- Extracting a Portion (Non-Mutating)
The slice() method returns a shallow copy of a portion of an array as a new array. It takes two optional arguments: the start index (inclusive) and the end index (exclusive). If you omit both, it copies the entire array. The original array is never modified. Do not confuse slice() with splice() -- they sound similar but behave very differently.
Example: Using slice()
const animals = ['ant', 'bear', 'cat', 'dog', 'eagle', 'fox'];
// Extract from index 2 to index 4 (exclusive)
const subset = animals.slice(2, 4);
console.log(subset); // ['cat', 'dog']
console.log(animals); // ['ant', 'bear', 'cat', 'dog', 'eagle', 'fox'] -- unchanged
// Extract from index 3 to the end
const fromThree = animals.slice(3);
console.log(fromThree); // ['dog', 'eagle', 'fox']
// Extract the last 2 elements using negative index
const lastTwo = animals.slice(-2);
console.log(lastTwo); // ['eagle', 'fox']
// Extract from second-to-last to last (exclusive)
const secondToLast = animals.slice(-3, -1);
console.log(secondToLast); // ['dog', 'eagle']
// Copy the entire array
const fullCopy = animals.slice();
console.log(fullCopy); // ['ant', 'bear', 'cat', 'dog', 'eagle', 'fox']
console.log(fullCopy === animals); // false (different reference)
// Common pattern: remove an element by index without mutating
const original = ['a', 'b', 'c', 'd', 'e'];
const indexToRemove = 2;
const withoutC = [...original.slice(0, indexToRemove), ...original.slice(indexToRemove + 1)];
console.log(withoutC); // ['a', 'b', 'd', 'e']
console.log(original); // ['a', 'b', 'c', 'd', 'e'] -- unchanged
slice() and splice() are often confused. Remember: slice is non-mutating and returns a portion (like slicing a piece of cake -- the cake still exists). splice is mutating and can add, remove, or replace elements in place (like splicing a rope -- the rope is permanently altered).flat() -- Flattening Nested Arrays (Non-Mutating)
The flat() method creates a new array with all sub-array elements concatenated into it recursively, up to the specified depth. The default depth is 1. To completely flatten a deeply nested array, pass Infinity as the depth. This is a non-mutating method introduced in ES2019.
Example: Using flat()
// Flatten one level deep (default)
const nested = [1, [2, 3], [4, [5, 6]]];
const flat1 = nested.flat();
console.log(flat1); // [1, 2, 3, 4, [5, 6]] -- only one level flattened
// Flatten two levels deep
const flat2 = nested.flat(2);
console.log(flat2); // [1, 2, 3, 4, 5, 6]
// Flatten to any depth with Infinity
const deeplyNested = [1, [2, [3, [4, [5]]]]];
const completelyFlat = deeplyNested.flat(Infinity);
console.log(completelyFlat); // [1, 2, 3, 4, 5]
// flat() also removes empty slots
const sparse = [1, , 3, , 5];
console.log(sparse.flat()); // [1, 3, 5]
// Practical: flatten grouped results
const departmentEmployees = [
['Ali', 'Sara'],
['Omar', 'Layla', 'Khalid'],
['Nour']
];
const allEmployees = departmentEmployees.flat();
console.log(allEmployees);
// ['Ali', 'Sara', 'Omar', 'Layla', 'Khalid', 'Nour']
// Original is unchanged
console.log(departmentEmployees[0]); // ['Ali', 'Sara']
flatMap() -- Map Then Flatten (Non-Mutating)
The flatMap() method first maps each element using a function, then flattens the result by one level. It is equivalent to calling map() followed by flat(1), but it is more efficient because it performs both operations in a single pass. This is extremely useful when your mapping function returns arrays.
Example: Using flatMap()
// Split sentences into individual words
const sentences = ['Hello world', 'How are you', 'JavaScript is awesome'];
const words = sentences.flatMap(sentence => sentence.split(' '));
console.log(words);
// ['Hello', 'world', 'How', 'are', 'you', 'JavaScript', 'is', 'awesome']
// Compare with map (produces nested arrays)
const wordsNested = sentences.map(sentence => sentence.split(' '));
console.log(wordsNested);
// [['Hello', 'world'], ['How', 'are', 'you'], ['JavaScript', 'is', 'awesome']]
// Duplicate each element
const nums = [1, 2, 3];
const duplicated = nums.flatMap(n => [n, n]);
console.log(duplicated); // [1, 1, 2, 2, 3, 3]
// Filter and transform in one step (return empty array to remove)
const scores = [85, 42, 91, 55, 78, 30, 95];
const passingGrades = scores.flatMap(score =>
score >= 60 ? [`Grade: ${score}`] : []
);
console.log(passingGrades);
// ['Grade: 85', 'Grade: 91', 'Grade: 78', 'Grade: 95']
// Expand items with their variations
const products = [
{ name: 'Shirt', sizes: ['S', 'M', 'L'] },
{ name: 'Hat', sizes: ['M', 'L'] }
];
const variants = products.flatMap(product =>
product.sizes.map(size => `${product.name} - ${size}`)
);
console.log(variants);
// ['Shirt - S', 'Shirt - M', 'Shirt - L', 'Hat - M', 'Hat - L']
Understanding Mutation vs Immutability in Practice
Now that you know both mutating and non-mutating methods, let us solidify when to use each approach. The choice depends on your context, coding style, and framework requirements.
Example: Mutating vs Immutable Patterns Side by Side
// --- ADDING ELEMENTS ---
// Mutating
const arr1 = [1, 2, 3];
arr1.push(4); // Add to end
arr1.unshift(0); // Add to beginning
arr1.splice(2, 0, 1.5); // Insert at index 2
console.log(arr1); // [0, 1, 1.5, 2, 3, 4]
// Immutable
const arr2 = [1, 2, 3];
const addEnd = [...arr2, 4]; // Add to end
const addStart = [0, ...arr2]; // Add to beginning
const addMiddle = [...arr2.slice(0, 2), 1.5, ...arr2.slice(2)]; // Insert at index 2
// --- REMOVING ELEMENTS ---
// Mutating
const arr3 = [1, 2, 3, 4, 5];
arr3.pop(); // Remove from end
arr3.shift(); // Remove from beginning
arr3.splice(1, 1); // Remove at index 1
console.log(arr3); // [2, 4]
// Immutable
const arr4 = [1, 2, 3, 4, 5];
const noLast = arr4.slice(0, -1); // Remove from end
const noFirst = arr4.slice(1); // Remove from beginning
const noIndex1 = [...arr4.slice(0, 1), ...arr4.slice(2)]; // Remove at index 1
// --- REPLACING ELEMENTS ---
// Mutating
const arr5 = ['a', 'b', 'c'];
arr5[1] = 'B'; // Direct replacement
arr5.splice(0, 1, 'A'); // Replace with splice
// Immutable
const arr6 = ['a', 'b', 'c'];
const replaced = arr6.map((item, i) => i === 1 ? 'B' : item);
const replacedWithSlice = ['A', ...arr6.slice(1)];
// --- SORTING ---
// Mutating
const arr7 = [3, 1, 2];
arr7.sort((a, b) => a - b);
// Immutable
const arr8 = [3, 1, 2];
const sorted = [...arr8].sort((a, b) => a - b);
// Or use toSorted() in ES2023+
const sorted2 = arr8.toSorted((a, b) => a - b);
toSorted() instead of sort(), toReversed() instead of reverse(), toSpliced() instead of splice(), and with(index, value) instead of direct index assignment. These are supported in all modern browsers and are the recommended approach for immutable operations.Real-World Example: Todo List Application
Let us build the data management layer for a todo list application, demonstrating how each method is used in a realistic scenario.
Example: Complete Todo List Data Layer
// Initialize the todo list
let todos = [
{ id: 1, text: 'Buy groceries', completed: false, priority: 'high' },
{ id: 2, text: 'Clean the house', completed: false, priority: 'medium' },
{ id: 3, text: 'Read a book', completed: true, priority: 'low' }
];
// ADD a new todo (push)
let nextId = 4;
function addTodo(text, priority = 'medium') {
todos.push({
id: nextId++,
text: text,
completed: false,
priority: priority
});
}
addTodo('Write code', 'high');
addTodo('Go for a walk', 'low');
console.log(todos.length); // 5
// ADD a todo at the top (unshift)
function addUrgentTodo(text) {
todos.unshift({
id: nextId++,
text: text,
completed: false,
priority: 'urgent'
});
}
addUrgentTodo('Fix critical bug');
console.log(todos[0].text); // 'Fix critical bug'
// REMOVE a todo by ID (splice)
function removeTodo(id) {
const index = todos.findIndex(todo => todo.id === id);
if (index !== -1) {
const removed = todos.splice(index, 1);
console.log(`Removed: "${removed[0].text}"`);
}
}
removeTodo(3); // Removes 'Read a book'
// TOGGLE completion (direct modification)
function toggleTodo(id) {
const todo = todos.find(t => t.id === id);
if (todo) {
todo.completed = !todo.completed;
}
}
toggleTodo(1);
console.log(todos.find(t => t.id === 1).completed); // true
// SORT todos: incomplete first, then by priority
const priorityOrder = { urgent: 0, high: 1, medium: 2, low: 3 };
function sortTodos() {
todos.sort((a, b) => {
// Incomplete todos come first
if (a.completed !== b.completed) return a.completed ? 1 : -1;
// Then sort by priority
return priorityOrder[a.priority] - priorityOrder[b.priority];
});
}
sortTodos();
console.log(todos.map(t => `[${t.completed ? 'x' : ' '}] ${t.priority}: ${t.text}`));
// GET completed todos (slice + filter pattern)
const completedTodos = todos.filter(t => t.completed);
console.log('Completed:', completedTodos.length);
// CLEAR all completed (splice in reverse to avoid index issues)
function clearCompleted() {
for (let i = todos.length - 1; i >= 0; i--) {
if (todos[i].completed) {
todos.splice(i, 1);
}
}
}
clearCompleted();
console.log('After clearing completed:', todos.length);
Real-World Example: Shopping Cart
Let us build a shopping cart system that demonstrates array methods in an e-commerce context.
Example: Shopping Cart Management
// Shopping cart data structure
let cart = [];
// Add item to cart (push)
function addToCart(product, quantity = 1) {
const existingIndex = cart.findIndex(item => item.productId === product.id);
if (existingIndex !== -1) {
// Item already in cart -- update quantity
cart[existingIndex].quantity += quantity;
console.log(`Updated ${product.name}: quantity = ${cart[existingIndex].quantity}`);
} else {
// New item -- add to cart
cart.push({
productId: product.id,
name: product.name,
price: product.price,
quantity: quantity
});
console.log(`Added ${product.name} to cart`);
}
}
// Product catalog
const products = [
{ id: 101, name: 'Laptop', price: 999.99 },
{ id: 102, name: 'Mouse', price: 29.99 },
{ id: 103, name: 'Keyboard', price: 79.99 },
{ id: 104, name: 'Monitor', price: 349.99 },
{ id: 105, name: 'Headphones', price: 149.99 }
];
// Add items to cart
addToCart(products[0]); // Laptop
addToCart(products[1], 2); // 2 Mice
addToCart(products[2]); // Keyboard
addToCart(products[4]); // Headphones
addToCart(products[1], 1); // Another Mouse (updates quantity to 3)
// Remove item from cart (splice)
function removeFromCart(productId) {
const index = cart.findIndex(item => item.productId === productId);
if (index !== -1) {
const removed = cart.splice(index, 1)[0];
console.log(`Removed ${removed.name} from cart`);
return removed;
}
return null;
}
// Update quantity (direct modification with splice fallback)
function updateQuantity(productId, newQuantity) {
const index = cart.findIndex(item => item.productId === productId);
if (index === -1) return;
if (newQuantity <= 0) {
// Remove item if quantity is zero or negative
cart.splice(index, 1);
} else {
cart[index].quantity = newQuantity;
}
}
// Calculate cart total
function getCartTotal() {
let total = 0;
for (let i = 0; i < cart.length; i++) {
total += cart[i].price * cart[i].quantity;
}
return total;
}
// Sort cart by price (highest first)
function sortCartByPrice() {
cart.sort((a, b) => (b.price * b.quantity) - (a.price * a.quantity));
}
// Get cart summary
function getCartSummary() {
sortCartByPrice();
const items = cart.map(item =>
`${item.name} x${item.quantity} = $${(item.price * item.quantity).toFixed(2)}`
);
return {
items: items,
itemCount: cart.reduce((sum, item) => sum + item.quantity, 0),
total: getCartTotal()
};
}
console.log(getCartSummary());
// { items: [...], itemCount: 5, total: 1349.94 }
// Save a snapshot of the cart (non-mutating copy with slice)
const savedCart = cart.slice();
// Empty the cart
cart.length = 0;
console.log(cart.length); // 0
console.log(savedCart.length); // 4 (snapshot preserved)
// Restore the cart from snapshot (push with spread)
cart.push(...savedCart);
console.log(cart.length); // 4 (restored)
// Merge carts from different sources (concat)
const wishlistItems = [
{ productId: 104, name: 'Monitor', price: 349.99, quantity: 1 }
];
const mergedCart = cart.concat(wishlistItems);
console.log(mergedCart.length); // 5
Practical Exercise
Build a complete task management system using array methods. Create an array of task objects, each with properties: id, title, status (pending/in-progress/done), assignee, and createdAt (use new Date().toISOString()). Implement the following operations: (1) Use push() to add 5 tasks. (2) Use unshift() to add an urgent task to the top. (3) Use pop() to remove the last task and log it. (4) Use shift() to remove the first task and log it. (5) Use splice() to remove a task by its index, insert 2 new tasks at index 2, and replace the task at index 1. (6) Use sort() with a compare function to sort tasks by status (pending first, then in-progress, then done). (7) Create a non-mutating sorted copy using the spread operator and sort(). (8) Use slice() to extract the first 3 tasks without modifying the original. (9) Use concat() to merge your tasks with a teammate's task array. (10) Create a nested array of tasks grouped by status, then use flat() to flatten them back. (11) Use flatMap() to create an array of strings like "Task: [title] - [status]" for all tasks, filtering out completed ones by returning empty arrays. (12) Use fill() to reset all task statuses to "pending". Test each operation and verify the results in your console.