How to Use Array Methods in JavaScript
Arrays in JavaScript come with a powerful set of built-in methods that handle virtually every data manipulation task you will encounter. From adding and removing elements to searching, transforming, filtering, sorting, and reducing data, these methods form the essential toolkit for working with collections.
Mastering array methods is one of the most impactful skills a JavaScript developer can build. They replace complex manual loops with expressive, readable, and often more efficient code. Instead of writing ten lines with a for loop, index management, and temporary variables, you can express the same logic in a single method call that clearly communicates your intent.
This guide covers every major array method in JavaScript, organized by purpose. Each method includes syntax, behavior, practical examples, and the critical distinction between methods that mutate the original array and those that return a new one.
Adding/Removing: splice, slice, concat
These three methods handle the core operations of adding, removing, and extracting elements from arrays. Despite their similar names, splice and slice behave very differently.
splice(start, deleteCount, ...items): The Swiss Army Knife
splice is the most versatile array modification method. It can remove elements, insert elements, or replace elements, all in a single call. It mutates the original array and returns an array of the removed elements.
let arr = ["a", "b", "c", "d", "e"];
// Remove 2 elements starting at index 1
let removed = arr.splice(1, 2);
console.log(arr); // ["a", "d", "e"]
console.log(removed); // ["b", "c"]
Insert without removing (set deleteCount to 0):
let arr = ["a", "d", "e"];
// Insert "b" and "c" at index 1, delete 0 elements
arr.splice(1, 0, "b", "c");
console.log(arr); // ["a", "b", "c", "d", "e"]
Replace elements:
let arr = ["a", "b", "c", "d", "e"];
// At index 1, remove 2 elements and insert "X", "Y", "Z"
arr.splice(1, 2, "X", "Y", "Z");
console.log(arr); // ["a", "X", "Y", "Z", "d", "e"]
Negative indices count from the end:
let arr = [1, 2, 3, 4, 5];
arr.splice(-2, 1); // Remove 1 element, 2 from the end
console.log(arr); // [1, 2, 3, 5]
Delete all from a position:
let arr = [1, 2, 3, 4, 5];
arr.splice(2); // Remove everything from index 2 onward
console.log(arr); // [1, 2]
slice(start, end): Extract Without Mutating
slice returns a new array containing elements from start up to (but not including) end. The original array is not modified.
let arr = ["a", "b", "c", "d", "e"];
console.log(arr.slice(1, 3)); // ["b", "c"]
console.log(arr.slice(2)); // ["c", "d", "e"] (from index 2 to end)
console.log(arr.slice(-2)); // ["d", "e"] (last 2 elements)
console.log(arr.slice(-3, -1)); // ["c", "d"]
console.log(arr.slice()); // ["a", "b", "c", "d", "e"] (shallow copy)
console.log(arr); // ["a", "b", "c", "d", "e"] (unchanged!)
slice() with no arguments creates a shallow copy of the entire array, which is a common pattern:
let original = [1, 2, 3];
let copy = original.slice();
copy.push(4);
console.log(original); // [1, 2, 3] (unaffected)
console.log(copy); // [1, 2, 3, 4]
concat(...items): Merge Arrays
concat creates a new array by merging the original with other arrays or values. It does not mutate the original.
let arr1 = [1, 2];
let arr2 = [3, 4];
let arr3 = [5, 6];
let merged = arr1.concat(arr2, arr3);
console.log(merged); // [1, 2, 3, 4, 5, 6]
console.log(arr1); // [1, 2] (unchanged)
// Can also add individual values
let extended = arr1.concat(3, 4, [5, 6]);
console.log(extended); // [1, 2, 3, 4, 5, 6]
// Only flattens one level
let nested = arr1.concat([3, [4, 5]]);
console.log(nested); // [1, 2, 3, [4, 5]]
The spread operator provides a modern alternative:
let merged = [...arr1, ...arr2, ...arr3];
console.log(merged); // [1, 2, 3, 4, 5, 6]
splice vs. slice Quick Reference
| Feature | splice | slice |
|---|---|---|
| Mutates original | Yes | No |
| Returns | Removed elements | New array (extracted portion) |
| Can insert/replace | Yes | No |
| Common use | Modify array in place | Extract sub-array or copy |
Searching: indexOf, lastIndexOf, includes
These methods search for a specific value in an array using strict equality (===).
indexOf(item, fromIndex)
Returns the first index where the item is found, or -1 if not found:
let arr = [1, 2, 3, 2, 1];
console.log(arr.indexOf(2)); // 1 (first occurrence)
console.log(arr.indexOf(2, 2)); // 3 (search starting from index 2)
console.log(arr.indexOf(99)); // -1 (not found)
lastIndexOf(item, fromIndex)
Returns the last index where the item is found:
let arr = [1, 2, 3, 2, 1];
console.log(arr.lastIndexOf(2)); // 3 (last occurrence)
console.log(arr.lastIndexOf(2, 2)); // 1 (searching backward from index 2)
includes(item, fromIndex)
Returns true or false. Simpler than checking indexOf !== -1:
let arr = [1, 2, 3, NaN];
console.log(arr.includes(2)); // true
console.log(arr.includes(99)); // false
// includes correctly finds NaN (indexOf cannot!)
console.log(arr.includes(NaN)); // true
console.log(arr.indexOf(NaN)); // -1 (indexOf FAILS with NaN!)
Use includes when you just need to know if a value exists. Use indexOf when you need the position. Note that includes correctly handles NaN, while indexOf does not.
Finding: find, findIndex, findLast, findLastIndex
These methods search using a callback function, making them far more flexible than indexOf/includes. They are essential for finding objects in arrays.
find(callback)
Returns the first element that satisfies the condition, or undefined if none match:
let users = [
{ id: 1, name: "Alice", age: 30 },
{ id: 2, name: "Bob", age: 25 },
{ id: 3, name: "Charlie", age: 35 }
];
let user = users.find(u => u.age > 28);
console.log(user); // { id: 1, name: "Alice", age: 30 }
let missing = users.find(u => u.age > 100);
console.log(missing); // undefined
findIndex(callback)
Returns the index of the first matching element, or -1:
let users = [
{ id: 1, name: "Alice" },
{ id: 2, name: "Bob" },
{ id: 3, name: "Charlie" }
];
let index = users.findIndex(u => u.name === "Bob");
console.log(index); // 1
let missing = users.findIndex(u => u.name === "Dave");
console.log(missing); // -1
findLast(callback) and findLastIndex(callback) (ES2023)
Search from the end of the array:
let numbers = [1, 5, 10, 15, 20, 25];
let lastUnder20 = numbers.findLast(n => n < 20);
console.log(lastUnder20); // 15
let lastUnder20Index = numbers.findLastIndex(n => n < 20);
console.log(lastUnder20Index); // 3
These are particularly useful when you know the element you want is closer to the end, or when you need the last match specifically.
find vs. filter
find returns the first match (a single element). filter returns all matches (an array). Choose based on whether you need one result or all results:
let numbers = [1, 5, 10, 15, 20, 25];
let firstOver10 = numbers.find(n => n > 10);
console.log(firstOver10); // 15 (single value)
let allOver10 = numbers.filter(n => n > 10);
console.log(allOver10); // [15, 20, 25] (array of all matches)
Transforming: map for creating new arrays
map creates a new array by applying a function to every element. It is one of the most frequently used array methods.
let numbers = [1, 2, 3, 4, 5];
let doubled = numbers.map(n => n * 2);
console.log(doubled); // [2, 4, 6, 8, 10]
console.log(numbers); // [1, 2, 3, 4, 5] (unchanged)
Practical Examples
// Extract specific properties
let users = [
{ name: "Alice", age: 30 },
{ name: "Bob", age: 25 },
{ name: "Charlie", age: 35 }
];
let names = users.map(u => u.name);
console.log(names); // ["Alice", "Bob", "Charlie"]
// Transform data format
let prices = [10, 20, 30];
let formatted = prices.map(p => `$${p.toFixed(2)}`);
console.log(formatted); // ["$10.00", "$20.00", "$30.00"]
// Create objects from values
let ids = [1, 2, 3];
let objects = ids.map(id => ({ id, active: true }));
console.log(objects);
// [{ id: 1, active: true }, { id: 2, active: true }, { id: 3, active: true }]
// Using index (second callback argument)
let letters = ["a", "b", "c"];
let indexed = letters.map((letter, i) => `${i}: ${letter}`);
console.log(indexed); // ["0: a", "1: b", "2: c"]
map is for transforming data. If you are using map without returning anything (just for side effects), use forEach instead. map without a return creates an array of undefined values.
// ❌ WRONG: Using map for side effects
users.map(u => console.log(u.name)); // Returns [undefined, undefined, undefined]
// ✅ CORRECT: Use forEach for side effects
users.forEach(u => console.log(u.name));
Filtering: filter for selecting elements
filter creates a new array containing only elements that pass a test function:
let numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
let evens = numbers.filter(n => n % 2 === 0);
console.log(evens); // [2, 4, 6, 8, 10]
let overFive = numbers.filter(n => n > 5);
console.log(overFive); // [6, 7, 8, 9, 10]
Filtering Objects
let products = [
{ name: "Laptop", price: 999, inStock: true },
{ name: "Phone", price: 699, inStock: false },
{ name: "Tablet", price: 499, inStock: true },
{ name: "Watch", price: 299, inStock: true }
];
let available = products.filter(p => p.inStock);
console.log(available.length); // 3
let affordable = products.filter(p => p.price < 500 && p.inStock);
console.log(affordable);
// [{ name: "Tablet", price: 499, inStock: true }, { name: "Watch", price: 299, inStock: true }]
Removing Falsy Values
let messy = [0, "hello", "", null, undefined, 42, false, "world", NaN];
let clean = messy.filter(Boolean);
console.log(clean); // ["hello", 42, "world"]
Boolean as a function returns true for truthy values and false for falsy ones, making it a concise filter predicate.
Removing Duplicates (Without Set)
let numbers = [1, 2, 3, 2, 1, 4, 5, 3];
let unique = numbers.filter((value, index, arr) => {
return arr.indexOf(value) === index;
});
console.log(unique); // [1, 2, 3, 4, 5]
Reducing: reduce, reduceRight, Accumulating Values
reduce is the most powerful array method. It processes each element and accumulates a single result, which can be any type: a number, string, object, or even another array.
Syntax
array.reduce((accumulator, currentValue, index, array) => {
// return updated accumulator
}, initialValue);
Basic Examples
// Sum all numbers
let numbers = [1, 2, 3, 4, 5];
let sum = numbers.reduce((acc, num) => acc + num, 0);
console.log(sum); // 15
// How it works step by step:
// acc=0, num=1 → return 1
// acc=1, num=2 → return 3
// acc=3, num=3 → return 6
// acc=6, num=4 → return 10
// acc=10, num=5 → return 15
// Product
let product = numbers.reduce((acc, num) => acc * num, 1);
console.log(product); // 120
// Maximum value
let max = numbers.reduce((acc, num) => num > acc ? num : acc, -Infinity);
console.log(max); // 5
Reducing to Objects
// Count occurrences
let fruits = ["apple", "banana", "apple", "cherry", "banana", "apple"];
let counts = fruits.reduce((acc, fruit) => {
acc[fruit] = (acc[fruit] || 0) + 1;
return acc;
}, {});
console.log(counts); // { apple: 3, banana: 2, cherry: 1 }
Reducing to Arrays (Alternative to filter + map)
let numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// Get doubled values of even numbers (filter + map in one pass)
let result = numbers.reduce((acc, num) => {
if (num % 2 === 0) {
acc.push(num * 2);
}
return acc;
}, []);
console.log(result); // [4, 8, 12, 16, 20]
Grouping with reduce
let people = [
{ name: "Alice", department: "Engineering" },
{ name: "Bob", department: "Marketing" },
{ name: "Charlie", department: "Engineering" },
{ name: "Diana", department: "Marketing" },
{ name: "Eve", department: "Design" }
];
let byDepartment = people.reduce((groups, person) => {
let dept = person.department;
if (!groups[dept]) groups[dept] = [];
groups[dept].push(person);
return groups;
}, {});
console.log(byDepartment);
// {
// Engineering: [{ name: "Alice"... }, { name: "Charlie"... }],
// Marketing: [{ name: "Bob"... }, { name: "Diana"... }],
// Design: [{ name: "Eve"... }]
// }
reduceRight
Works the same as reduce but processes elements from right to left:
let arr = [[1, 2], [3, 4], [5, 6]];
let flattened = arr.reduceRight((acc, val) => acc.concat(val), []);
console.log(flattened); // [5, 6, 3, 4, 1, 2]
Without Initial Value
If no initial value is provided, the first element becomes the initial accumulator and iteration starts from the second element:
let numbers = [1, 2, 3, 4, 5];
let sum = numbers.reduce((acc, num) => acc + num);
console.log(sum); // 15 (works, but...)
// ❌ DANGER: Throws on empty arrays without initial value!
// [].reduce((acc, num) => acc + num);
// TypeError: Reduce of empty array with no initial value
// ✅ SAFE: Always provide an initial value
[].reduce((acc, num) => acc + num, 0); // 0
Calling reduce on an empty array without an initial value throws a TypeError. Always provide the second argument to reduce to handle edge cases safely.
Testing: every, some
These methods test whether array elements satisfy a condition, returning a boolean.
every(callback): Do ALL Elements Pass?
Returns true only if every element passes the test:
let numbers = [2, 4, 6, 8, 10];
console.log(numbers.every(n => n % 2 === 0)); // true (all are even)
console.log(numbers.every(n => n > 5)); // false (2 and 4 fail)
let ages = [18, 21, 25, 30];
console.log(ages.every(age => age >= 18)); // true (all adults)
// Empty arrays always return true (vacuous truth)
console.log([].every(n => n > 100)); // true
some(callback): Does ANY Element Pass?
Returns true if at least one element passes the test:
let numbers = [1, 3, 5, 7, 8];
console.log(numbers.some(n => n % 2 === 0)); // true (8 is even)
console.log(numbers.some(n => n > 100)); // false (none pass)
// Practical: check for errors
let responses = [200, 200, 404, 200];
let hasError = responses.some(code => code >= 400);
console.log(hasError); // true
// Empty arrays always return false
console.log([].some(n => n > 0)); // false
Short-Circuit Behavior
Both methods stop early once the result is determined:
let data = [1, 2, 3, 4, 5];
// every stops at the first false
data.every(n => {
console.log(`Checking ${n}`);
return n < 3;
});
// Checking 1
// Checking 2
// Checking 3 (stops here (returned false))
// some stops at the first true
data.some(n => {
console.log(`Checking ${n}`);
return n > 3;
});
// Checking 1
// Checking 2
// Checking 3
// Checking 4 (stops here (returned true))
Iterating: forEach
forEach executes a function for each element. It is purely for side effects and always returns undefined.
let fruits = ["apple", "banana", "cherry"];
fruits.forEach((fruit, index) => {
console.log(`${index}: ${fruit}`);
});
// 0: apple
// 1: banana
// 2: cherry
forEach vs. for...of
The key limitation of forEach is that you cannot use break, continue, or return to control the loop flow:
// ❌ Cannot break out of forEach
let numbers = [1, 2, 3, 4, 5];
numbers.forEach(n => {
if (n === 3) return; // This only skips the current callback, doesn't stop the loop!
console.log(n);
});
// 1, 2, 4, 5 ("return" acts like "continue", not "break")
// ✅ Use for...of when you need break
for (let n of numbers) {
if (n === 3) break;
console.log(n);
}
// 1, 2
Use forEach for simple iteration where you need to process every element. Use for...of when you might need to stop early.
Sorting: sort, Custom Comparators, Stability, and Gotchas
The sort method sorts the array in place (mutates) and returns the sorted array.
The Default Sorting Gotcha
Without a comparator function, sort converts elements to strings and sorts them lexicographically (by Unicode code point order):
let numbers = [10, 9, 80, 1, 21, 3];
numbers.sort();
console.log(numbers); // [1, 10, 21, 3, 80, 9] (WRONG for numeric sorting!)
// "10" < "21" < "3" < "80" < "9" in string comparison
This is one of the most common JavaScript gotchas. Always provide a comparator for numeric sorting.
Numeric Sorting
let numbers = [10, 9, 80, 1, 21, 3];
// Ascending
numbers.sort((a, b) => a - b);
console.log(numbers); // [1, 3, 9, 10, 21, 80]
// Descending
numbers.sort((a, b) => b - a);
console.log(numbers); // [80, 21, 10, 9, 3, 1]
The comparator function should return:
- A negative number if
ashould come beforeb - Zero if they are equal
- A positive number if
ashould come afterb
Sorting Strings
let words = ["banana", "Apple", "cherry", "date"];
// Default: uppercase letters come before lowercase (by code point)
words.sort();
console.log(words); // ["Apple", "banana", "cherry", "date"]
// Case-insensitive sorting
words.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
console.log(words); // ["Apple", "banana", "cherry", "date"]
// Locale-aware sorting
words.sort((a, b) => a.localeCompare(b));
console.log(words); // ["Apple", "banana", "cherry", "date"]
Sorting Objects
let users = [
{ name: "Charlie", age: 35 },
{ name: "Alice", age: 30 },
{ name: "Bob", age: 25 }
];
// Sort by age (ascending)
users.sort((a, b) => a.age - b.age);
console.log(users.map(u => u.name)); // ["Bob", "Alice", "Charlie"]
// Sort by name (alphabetical)
users.sort((a, b) => a.name.localeCompare(b.name));
console.log(users.map(u => u.name)); // ["Alice", "Bob", "Charlie"]
Stability
Modern JavaScript engines guarantee stable sorting: elements that are equal according to the comparator maintain their original relative order. This has been required since ES2019.
let items = [
{ name: "A", priority: 1 },
{ name: "B", priority: 2 },
{ name: "C", priority: 1 },
{ name: "D", priority: 2 }
];
items.sort((a, b) => a.priority - b.priority);
console.log(items.map(i => i.name)); // ["A", "C", "B", "D"]
// A and C (both priority 1) kept their original order
// B and D (both priority 2) kept their original order
sort() modifies the array in place. If you need to keep the original unchanged, copy first: let sorted = [...arr].sort(...) or use toSorted() (ES2023).
Reversing: reverse, toReversed
reverse(): Mutating
Reverses the array in place:
let arr = [1, 2, 3, 4, 5];
arr.reverse();
console.log(arr); // [5, 4, 3, 2, 1] (mutated!)
toReversed(): Non-Mutating (ES2023)
Returns a new reversed array without modifying the original:
let arr = [1, 2, 3, 4, 5];
let reversed = arr.toReversed();
console.log(reversed); // [5, 4, 3, 2, 1]
console.log(arr); // [1, 2, 3, 4, 5] (unchanged!)
Flattening: flat, flatMap
flat(depth)
Creates a new array with nested arrays flattened to the specified depth:
let nested = [1, [2, 3], [4, [5, 6]]];
console.log(nested.flat()); // [1, 2, 3, 4, [5, 6]] (depth 1 (default))
console.log(nested.flat(2)); // [1, 2, 3, 4, 5, 6] (depth 2)
console.log(nested.flat(Infinity)); // [1, 2, 3, 4, 5, 6] (flatten all levels)
// Removes holes in sparse arrays
let sparse = [1, , 3, , 5];
console.log(sparse.flat()); // [1, 3, 5]
flatMap(callback)
Combines map and flat(1) in a single step. Maps each element, then flattens the result by one level:
let sentences = ["Hello world", "How are you"];
// map returns nested arrays
let words = sentences.map(s => s.split(" "));
console.log(words); // [["Hello", "world"], ["How", "are", "you"]]
// flatMap flattens one level
let flatWords = sentences.flatMap(s => s.split(" "));
console.log(flatWords); // ["Hello", "world", "How", "are", "you"]
Practical uses:
// Expanding items
let orders = [
{ id: 1, items: ["apple", "banana"] },
{ id: 2, items: ["cherry"] },
{ id: 3, items: ["date", "elderberry", "fig"] }
];
let allItems = orders.flatMap(order => order.items);
console.log(allItems); // ["apple", "banana", "cherry", "date", "elderberry", "fig"]
// Filter and map simultaneously (return empty array to exclude)
let numbers = [1, 2, 3, 4, 5, 6];
let doubledEvens = numbers.flatMap(n => n % 2 === 0 ? [n * 2] : []);
console.log(doubledEvens); // [4, 8, 12]
Converting: Array.from, Array.of
Array.from(iterable, mapFn)
Creates a new array from an iterable or array-like object:
// From a string
console.log(Array.from("hello")); // ["h", "e", "l", "l", "o"]
// From a Set
let set = new Set([1, 2, 3, 2, 1]);
console.log(Array.from(set)); // [1, 2, 3]
// From a Map
let map = new Map([["a", 1], ["b", 2]]);
console.log(Array.from(map)); // [["a", 1], ["b", 2]]
// From array-like objects (NodeList, arguments, etc.)
// let divs = Array.from(document.querySelectorAll("div"));
// With a mapping function (second argument)
console.log(Array.from({ length: 5 }, (_, i) => i + 1));
// [1, 2, 3, 4, 5]
console.log(Array.from({ length: 3 }, () => Math.random()));
// [0.123, 0.456, 0.789] (three random numbers)
Array.of(...items)
Creates an array from its arguments, avoiding the new Array(n) ambiguity:
console.log(Array.of(3)); // [3] (one element with value 3)
console.log(Array.of(1, 2, 3)); // [1, 2, 3]
// Compare with Array constructor:
console.log(new Array(3)); // [empty × 3] (confusing!)
Filling: fill, copyWithin
fill(value, start, end)
Fills array positions with a static value. Mutates the original:
let arr = [1, 2, 3, 4, 5];
arr.fill(0);
console.log(arr); // [0, 0, 0, 0, 0]
arr.fill(9, 2, 4);
console.log(arr); // [0, 0, 9, 9, 0]
// Common: create array of specific size with default value
let zeros = new Array(5).fill(0);
console.log(zeros); // [0, 0, 0, 0, 0]
// ❌ WRONG: All elements reference the SAME object
let grid = new Array(3).fill([]);
grid[0].push("X");
console.log(grid); // [["X"], ["X"], ["X"]] (all changed!)
// ✅ CORRECT: Use Array.from to create independent objects
let grid2 = Array.from({ length: 3 }, () => []);
grid2[0].push("X");
console.log(grid2); // [["X"], [], []]
copyWithin(target, start, end)
Copies a portion of the array to another position within the same array. Mutates without changing the length:
let arr = [1, 2, 3, 4, 5];
// Copy elements at index 3-4 to index 0
arr.copyWithin(0, 3);
console.log(arr); // [4, 5, 3, 4, 5]
copyWithin is rarely used in everyday code but can be efficient for buffer manipulation.
Grouping: Object.groupBy, Map.groupBy (ES2024)
ES2024 introduced dedicated grouping methods:
Object.groupBy(iterable, callback)
let people = [
{ name: "Alice", department: "Engineering" },
{ name: "Bob", department: "Marketing" },
{ name: "Charlie", department: "Engineering" },
{ name: "Diana", department: "Design" }
];
let grouped = Object.groupBy(people, person => person.department);
console.log(grouped);
// {
// Engineering: [{ name: "Alice"... }, { name: "Charlie"... }],
// Marketing: [{ name: "Bob"... }],
// Design: [{ name: "Diana"... }]
// }
Map.groupBy(iterable, callback)
Returns a Map instead of a plain object, useful when keys are not strings:
let numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9];
let grouped = Map.groupBy(numbers, n => n % 2 === 0 ? "even" : "odd");
console.log(grouped.get("even")); // [2, 4, 6, 8]
console.log(grouped.get("odd")); // [1, 3, 5, 7, 9]
These methods replace the common reduce-based grouping pattern with a cleaner, more readable API.
New Immutable Methods: toSorted, toReversed, toSpliced, with (ES2023)
ES2023 introduced non-mutating versions of several traditionally mutating methods. These return new arrays, leaving the original untouched.
toSorted(compareFn)
Non-mutating version of sort():
let numbers = [3, 1, 4, 1, 5, 9];
let sorted = numbers.toSorted((a, b) => a - b);
console.log(sorted); // [1, 1, 3, 4, 5, 9]
console.log(numbers); // [3, 1, 4, 1, 5, 9] (unchanged!)
toReversed()
Non-mutating version of reverse():
let arr = [1, 2, 3];
let reversed = arr.toReversed();
console.log(reversed); // [3, 2, 1]
console.log(arr); // [1, 2, 3] (unchanged!)
toSpliced(start, deleteCount, ...items)
Non-mutating version of splice():
let arr = ["a", "b", "c", "d", "e"];
let result = arr.toSpliced(1, 2, "X", "Y");
console.log(result); // ["a", "X", "Y", "d", "e"]
console.log(arr); // ["a", "b", "c", "d", "e"] (nchanged!)
with(index, value)
Returns a new array with one element replaced at the specified index:
let arr = ["a", "b", "c", "d"];
let updated = arr.with(1, "X");
console.log(updated); // ["a", "X", "c", "d"]
console.log(arr); // ["a", "b", "c", "d"] (unchanged!)
// Supports negative indices
let last = arr.with(-1, "Z");
console.log(last); // ["a", "b", "c", "Z"]
Mutating vs. Non-Mutating Comparison
| Mutating | Non-Mutating (ES2023) |
|---|---|
sort() | toSorted() |
reverse() | toReversed() |
splice() | toSpliced() |
arr[i] = value | with(i, value) |
The non-mutating versions are particularly valuable in React, Redux, and other frameworks where immutability is important. They eliminate the need for [...arr].sort() or arr.slice().reverse() patterns.
Chaining Array Methods: Composing Transformations
One of the most powerful aspects of array methods is their ability to be chained together. Since most non-mutating methods return arrays, you can pipe the output of one directly into the next:
let users = [
{ name: "Alice", age: 30, active: true },
{ name: "Bob", age: 17, active: true },
{ name: "Charlie", age: 35, active: false },
{ name: "Diana", age: 28, active: true },
{ name: "Eve", age: 22, active: true }
];
// Get names of active adult users, sorted alphabetically
let result = users
.filter(u => u.active) // Keep only active users
.filter(u => u.age >= 18) // Keep only adults
.map(u => u.name) // Extract names
.sort(); // Sort alphabetically
console.log(result); // ["Alice", "Diana", "Eve"]
A More Complex Chain
let orders = [
{ product: "Laptop", price: 999, quantity: 2 },
{ product: "Phone", price: 699, quantity: 1 },
{ product: "Cable", price: 9.99, quantity: 10 },
{ product: "Mouse", price: 29, quantity: 5 },
{ product: "Keyboard", price: 79, quantity: 3 }
];
let summary = orders
.map(order => ({
...order,
total: order.price * order.quantity
}))
.filter(order => order.total > 100)
.toSorted((a, b) => b.total - a.total)
.map(order => `${order.product}: $${order.total.toFixed(2)}`);
console.log(summary);
// ["Laptop: $1998.00", "Phone: $699.00", "Keyboard: $237.00", "Mouse: $145.00"]
When Chaining Gets Too Long
If a chain becomes hard to read, break it into named intermediate steps:
// ✅ Sometimes intermediate variables improve readability
let activeUsers = users.filter(u => u.active);
let adults = activeUsers.filter(u => u.age >= 18);
let names = adults.map(u => u.name);
let sorted = names.sort();
Performance Considerations and When to Use Which Method
Choosing the Right Method
| Goal | Method | Returns |
|---|---|---|
| Transform every element | map | New array |
| Keep elements matching condition | filter | New array |
| Find first matching element | find | Single element or undefined |
| Find index of first match | findIndex | Index or -1 |
| Check if value exists | includes | Boolean |
| Check if any element passes | some | Boolean |
| Check if all elements pass | every | Boolean |
| Accumulate into single value | reduce | Any type |
| Execute side effects | forEach | undefined |
| Add to end | push | New length |
| Remove from end | pop | Removed element |
| Sort in place | sort | Same array (mutated) |
| Sort without mutating | toSorted | New array |
Performance Tips
Early termination: find, findIndex, some, and every stop as soon as they get a result. Use them instead of filter when you only need the first match or a boolean answer:
let numbers = [1, 2, 3, 4, 5, ...new Array(1000000).fill(6)];
// ❌ Slow: filter processes ALL elements, then checks length
let hasEven = numbers.filter(n => n % 2 === 0).length > 0;
// ✅ Fast: some stops at the first even number (index 1)
let hasEven2 = numbers.some(n => n % 2 === 0);
Avoid multiple passes when one will do:
let numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// ❌ Two passes through the array
let result = numbers
.filter(n => n % 2 === 0)
.map(n => n * 2);
// ✅ Single pass with reduce
let result2 = numbers.reduce((acc, n) => {
if (n % 2 === 0) acc.push(n * 2);
return acc;
}, []);
// ✅ Single pass with flatMap
let result3 = numbers.flatMap(n => n % 2 === 0 ? [n * 2] : []);
For small to medium arrays (hundreds to low thousands of elements), the readability of chaining filter then map far outweighs the performance difference. The reduce optimization matters only for very large arrays or performance-critical code paths.
Mutating vs. creating new arrays:
Mutating methods (sort, reverse, splice, push, pop) are slightly faster because they do not allocate new arrays. Non-mutating methods (toSorted, toReversed, map, filter) allocate new memory. For most applications, this difference is negligible. Prefer immutability for code clarity unless profiling shows it is a bottleneck.
Summary
- splice mutates the array by adding, removing, or replacing elements. slice extracts a portion without mutating. concat merges arrays into a new one.
- indexOf, includes search for values with strict equality.
includeshandlesNaNcorrectly. - find and findIndex search using a callback function, essential for finding objects. findLast and findLastIndex (ES2023) search from the end.
- map transforms every element into a new array. Always return a value from the callback.
- filter selects elements matching a condition into a new array.
- reduce accumulates all elements into a single value of any type. Always provide an initial value.
- every checks if all elements pass a test. some checks if at least one passes. Both short-circuit.
- forEach executes side effects for each element. Cannot be stopped with
break. - sort mutates the array and defaults to string comparison. Always provide a numeric comparator for numbers.
- flat flattens nested arrays. flatMap combines mapping and flattening.
- Array.from converts iterables and array-likes to arrays. Array.of creates arrays from arguments.
- fill sets elements to a static value. Beware of shared references with objects.
- Object.groupBy and Map.groupBy (ES2024) provide clean grouping syntax.
- ES2023 immutable methods (toSorted, toReversed, toSpliced, with) return new arrays instead of mutating, ideal for frameworks requiring immutability.
- Chain methods for expressive data pipelines. Break long chains into named variables when readability suffers.
- For performance, use early-terminating methods (
find,some,every) when possible, and prefer single-pass operations for very large arrays.