JavaScript Array Methods You Must Know
You get a list of orders back from an API and need the paid ones, grouped by customer, with totals. The first version is usually a for loop with three temporary variables and a counter, and a week later nobody wants to touch it. Then a sort() somewhere quietly reorders an array another component was still using, and you spend an afternoon figuring out why the UI jumped.
This post goes through the array methods I actually use day to day: what each one returns, whether it mutates the original, and the gotchas that bite in real code. There are visual examples for the trickier ones and a short section on performance at the end.
- Transformation methods:
map(),flatMap() - Filtering methods:
filter(),find(),findIndex() - Aggregation methods:
reduce(),reduceRight() - Testing methods:
some(),every(),includes() - Iteration methods:
forEach(),entries(),keys() - Mutating vs non-mutating: knowing when arrays change
- Method chaining for readable data pipelines
Understanding Array Method Categories
When I'm not sure which method to reach for, I ask what I want back: a new array, one value, one element, or a yes/no. That question maps pretty cleanly onto these groups:
Transformers
Create new arrays with modified elements
Filters
Select elements that match criteria
Reducers
Combine elements into a single value
Searchers
Find specific elements or indices
Testers
Check conditions across elements
Iterators
Execute operations on each element
Most array methods don't modify the original array. They return a new one.
1. The map() Method: Transform Every Element
Any time the output should have the same length as the input but each item looks different, use map(). It calls your function on every element and collects whatever you return into a new array.
Syntax
const newArray = array.map((element, index, array) => {
// return transformed element
});
Visual Example: Doubling Numbers
// Basic transformation
const numbers = [1, 2, 3, 4, 5];
const doubled = numbers.map(num => num * 2);
// Result: [2, 4, 6, 8, 10]
// Transform objects
const users = [
{ name: 'Alice', age: 25 },
{ name: 'Bob', age: 30 },
{ name: 'Charlie', age: 35 }
];
const names = users.map(user => user.name);
// Result: ['Alice', 'Bob', 'Charlie']
// Add computed properties
const enrichedUsers = users.map(user => ({
...user,
isAdult: user.age >= 18,
greeting: `Hello, ${user.name}!`
}));
// Using index parameter
const indexed = ['a', 'b', 'c'].map((letter, index) => ({
position: index + 1,
letter: letter.toUpperCase()
}));
// Result: [{position: 1, letter: 'A'}, {position: 2, letter: 'B'}, ...]
If you aren't using the returned array, don't use map(). For side effects, forEach() says what you mean. The other classic mistake is an arrow function with braces and no return: users.map(u => { u.name }) gives you an array full of undefined.
2. The filter() Method: Keep What Matches
filter() returns a new array with only the elements where your callback returns something truthy. The output can be shorter than the input, or empty, but never longer.
Visual Example: Filter Even Numbers
// Filter by condition
const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const evenNumbers = numbers.filter(num => num % 2 === 0);
// Result: [2, 4, 6, 8, 10]
const greaterThanFive = numbers.filter(num => num > 5);
// Result: [6, 7, 8, 9, 10]
// Filter objects
const 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 }
];
const availableProducts = products.filter(p => p.inStock);
const affordableProducts = products.filter(p => p.price < 500);
// Multiple conditions
const affordableAvailable = products.filter(
p => p.inStock && p.price < 700
);
// Remove falsy values
const mixed = [0, 'hello', '', null, 42, undefined, 'world'];
const truthy = mixed.filter(Boolean);
// Result: ['hello', 42, 'world']
// Remove duplicates (for primitives, [...new Set(arr)] is simpler)
const duplicates = [1, 2, 2, 3, 3, 3, 4];
const unique = duplicates.filter((val, idx, arr) =>
arr.indexOf(val) === idx
);
// Result: [1, 2, 3, 4]
3. The reduce() Method: The Swiss Army Knife
When the result isn't an array of the same shape (a sum, a lookup object, a grouped structure), reduce() is usually the answer. It walks the array and carries an accumulator from one step to the next. You can technically rebuild map() and filter() with it, but that flexibility is also why reduce() code gets hard to read fast.
Syntax
const result = array.reduce((accumulator, current, index, array) => {
// return updated accumulator
}, initialValue);
Reduce accumulates values step by step, passing the result to the next iteration
// Sum all numbers
const numbers = [1, 2, 3, 4, 5];
const sum = numbers.reduce((acc, curr) => acc + curr, 0);
// Result: 15
// Find maximum value
const max = numbers.reduce((acc, curr) =>
curr > acc ? curr : acc,
numbers[0]
);
// Result: 5
// Count occurrences
const fruits = ['apple', 'banana', 'apple', 'orange', 'banana', 'apple'];
const fruitCount = fruits.reduce((acc, fruit) => {
acc[fruit] = (acc[fruit] || 0) + 1;
return acc;
}, {});
// Result: { apple: 3, banana: 2, orange: 1 }
// Group by property
const people = [
{ name: 'Alice', department: 'Engineering' },
{ name: 'Bob', department: 'Marketing' },
{ name: 'Charlie', department: 'Engineering' },
{ name: 'Diana', department: 'Marketing' }
];
const byDepartment = people.reduce((acc, person) => {
const dept = person.department;
acc[dept] = acc[dept] || [];
acc[dept].push(person);
return acc;
}, {});
// Flatten nested arrays
const nested = [[1, 2], [3, 4], [5, 6]];
const flat = nested.reduce((acc, arr) => [...acc, ...arr], []);
// Result: [1, 2, 3, 4, 5, 6]
// Build objects from arrays
const pairs = [['name', 'John'], ['age', 30], ['city', 'NYC']];
const obj = pairs.reduce((acc, [key, value]) => {
acc[key] = value;
return acc;
}, {});
// Result: { name: 'John', age: 30, city: 'NYC' }
Without one, the first element becomes the accumulator. That works until the array is empty, and then you get TypeError: Reduce of empty array with no initial value, typically in production when a filter upstream returned nothing. It also causes type mix-ups when the accumulator should be an object but starts as the first item. One more thing: the "flatten" example above spreads acc on every step, which copies the whole array each time. Fine for small inputs, slow for big ones. Use flat() instead.
4. The find() and findIndex() Methods: Get One Match
filter() returns every match. find() returns only the first one, and findIndex() returns its position. Both stop as soon as they hit a match, so they do less work when one result is all you need.
find()
Returns the first matching element, or undefined if none found
findIndex()
Returns the index of first match, or -1 if none found
const users = [
{ id: 1, name: 'Alice', role: 'admin' },
{ id: 2, name: 'Bob', role: 'user' },
{ id: 3, name: 'Charlie', role: 'user' },
{ id: 4, name: 'Diana', role: 'moderator' }
];
// Find first admin
const admin = users.find(user => user.role === 'admin');
// Result: { id: 1, name: 'Alice', role: 'admin' }
// Find user by ID
const user = users.find(u => u.id === 3);
// Result: { id: 3, name: 'Charlie', role: 'user' }
// Find index
const moderatorIndex = users.findIndex(u => u.role === 'moderator');
// Result: 3
// Check if exists (cleaner than findIndex !== -1)
const hasAdmin = users.some(u => u.role === 'admin');
// Result: true
// Use with nullish coalescing for defaults
const guest = users.find(u => u.role === 'guest') ?? { name: 'Anonymous' };
// Result: { name: 'Anonymous' }
// findLast() and findLastIndex() - search from end (ES2023)
const numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1];
const lastThree = numbers.findLast(n => n === 3);
// Result: 3 (the second occurrence)
const lastThreeIndex = numbers.findLastIndex(n => n === 3);
// Result: 6
5. The some() and every() Methods: Test Conditions
Need a yes/no answer about the whole array? some() is the array version of OR, every() is AND. Both short-circuit. Watch out for empty arrays: [].every(fn) is true and [].some(fn) is false, so an empty cart passes an "all items in stock" check.
Visual Comparison
some() - At least ONE must pass:
every() - ALL must pass:
const numbers = [1, 2, 3, 4, 5];
// some() - returns true if ANY element passes
const hasEven = numbers.some(n => n % 2 === 0);
// Result: true
const hasNegative = numbers.some(n => n < 0);
// Result: false
// every() - returns true if ALL elements pass
const allPositive = numbers.every(n => n > 0);
// Result: true
const allEven = numbers.every(n => n % 2 === 0);
// Result: false
// Practical examples
const cart = [
{ name: 'Laptop', price: 999, inStock: true },
{ name: 'Mouse', price: 29, inStock: true },
{ name: 'Keyboard', price: 79, inStock: false }
];
// Can we ship the order?
const canShip = cart.every(item => item.inStock);
// Result: false (keyboard not in stock)
// Is there anything expensive?
const hasExpensiveItem = cart.some(item => item.price > 500);
// Result: true
// Form validation
const formFields = [
{ name: 'email', valid: true },
{ name: 'password', valid: true },
{ name: 'username', valid: false }
];
const isFormValid = formFields.every(field => field.valid);
// Result: false
// Check permissions
const userPermissions = ['read', 'write'];
const requiredPermissions = ['read', 'write', 'delete'];
const hasAllPermissions = requiredPermissions.every(
perm => userPermissions.includes(perm)
);
// Result: false (missing 'delete')
6. The forEach() Method: Side Effects Only
forEach() runs a function on each element and returns nothing. It's for side effects like logging or DOM updates. Be careful with async callbacks: forEach doesn't wait for promises, so items.forEach(async item => await save(item)) fires everything at once and moves on before any of it finishes. Use for...of with await, or Promise.all(items.map(...)).
const users = ['Alice', 'Bob', 'Charlie'];
// Logging
users.forEach(user => console.log(`Hello, ${user}!`));
// DOM manipulation
users.forEach(user => {
const li = document.createElement('li');
li.textContent = user;
document.querySelector('ul').appendChild(li);
});
// With index
users.forEach((user, index) => {
console.log(`${index + 1}. ${user}`);
});
// Modifying external state (use with caution!)
let total = 0;
[10, 20, 30].forEach(n => total += n);
console.log(total); // 60
- Cannot use
breakorcontinue - Cannot return a value (returns
undefined) - Not chainable with other array methods
- If you need these, use
for...ofloop instead
7. The includes(), indexOf(), and lastIndexOf() Methods
When you're looking for an exact value instead of something matching a condition, these are simpler than find(). Keep in mind they compare by identity, so users.includes({ id: 1 }) is always false for objects.
const fruits = ['apple', 'banana', 'orange', 'banana', 'mango'];
// includes() - returns boolean
fruits.includes('banana'); // true
fruits.includes('grape'); // false
fruits.includes('banana', 3); // true (start from index 3)
// indexOf() - returns first index or -1
fruits.indexOf('banana'); // 1
fruits.indexOf('grape'); // -1
// lastIndexOf() - returns last index or -1
fruits.lastIndexOf('banana'); // 3
// Practical: toggle item in array
function toggleItem(array, item) {
const index = array.indexOf(item);
if (index === -1) {
return [...array, item]; // Add item
} else {
return array.filter((_, i) => i !== index); // Remove item
}
}
// Check membership (cleaner with includes)
const allowedRoles = ['admin', 'moderator', 'editor'];
const userRole = 'admin';
if (allowedRoles.includes(userRole)) {
console.log('Access granted');
}
// NaN handling - includes() works, indexOf() doesn't!
const withNaN = [1, NaN, 3];
withNaN.includes(NaN); // true ✓
withNaN.indexOf(NaN); // -1 ✗
8. The flat() and flatMap() Methods: Handle Nested Arrays
flat() removes nesting (one level by default). flatMap() does a map() followed by a one-level flat() in a single pass, which also makes it a handy way to filter and map at the same time.
Visual: Flattening Nested Arrays
[[1,2], [3,4], [5,6]]
→ .flat() →
[1, 2, 3, 4, 5, 6]
// flat() - flatten nested arrays
const nested = [[1, 2], [3, 4], [5, 6]];
const flat = nested.flat();
// Result: [1, 2, 3, 4, 5, 6]
// Deep nesting - specify depth
const deepNested = [1, [2, [3, [4, [5]]]]];
deepNested.flat(); // [1, 2, [3, [4, [5]]]] - depth 1 (default)
deepNested.flat(2); // [1, 2, 3, [4, [5]]]
deepNested.flat(Infinity); // [1, 2, 3, 4, 5] - flatten completely
// flatMap() - map + flat in one step
const sentences = ['Hello world', 'How are you'];
const words = sentences.flatMap(sentence => sentence.split(' '));
// Result: ['Hello', 'world', 'How', 'are', 'you']
// vs doing it separately (less efficient)
const wordsLong = sentences.map(s => s.split(' ')).flat();
// Practical: expand data
const users = [
{ name: 'Alice', emails: ['alice@work.com', 'alice@home.com'] },
{ name: 'Bob', emails: ['bob@work.com'] }
];
const allEmails = users.flatMap(user => user.emails);
// Result: ['alice@work.com', 'alice@home.com', 'bob@work.com']
// Filter while mapping
const numbers = [1, 2, 3, 4, 5];
const doubledEvens = numbers.flatMap(n =>
n % 2 === 0 ? [n * 2] : []
);
// Result: [4, 8] - only evens, doubled
9. The sort() Method: Order Your Data
sort() sorts in place and returns the same array. That mutation is the source of a lot of bugs, especially in React, where sorting an array from props or state changes data other code still holds. Spread first, or use toSorted() (ES2023).
Without a compare function, sort() converts elements to strings and sorts alphabetically. This means [10, 2, 1].sort() returns [1, 10, 2], not [1, 2, 10]!
// Default string sort (alphabetical)
const fruits = ['banana', 'apple', 'cherry'];
fruits.sort();
// Result: ['apple', 'banana', 'cherry']
// Number sort - MUST provide compare function
const numbers = [10, 2, 1, 21, 5];
// Wrong! Default converts to strings
numbers.sort();
// Result: [1, 10, 2, 21, 5] ❌
// Correct - ascending
numbers.sort((a, b) => a - b);
// Result: [1, 2, 5, 10, 21] ✓
// Descending
numbers.sort((a, b) => b - a);
// Result: [21, 10, 5, 2, 1]
// Sort objects by property
const products = [
{ name: 'Laptop', price: 999 },
{ name: 'Phone', price: 699 },
{ name: 'Tablet', price: 499 }
];
// By price (ascending)
products.sort((a, b) => a.price - b.price);
// By name (alphabetical)
products.sort((a, b) => a.name.localeCompare(b.name));
// Immutable sort (ES2023)
const original = [3, 1, 2];
const sorted = original.toSorted((a, b) => a - b);
// original: [3, 1, 2] (unchanged)
// sorted: [1, 2, 3]
// Or with spread
const sortedCopy = [...original].sort((a, b) => a - b);
// Complex multi-field sort
const users = [
{ name: 'Bob', age: 30 },
{ name: 'Alice', age: 25 },
{ name: 'Alice', age: 30 }
];
users.sort((a, b) => {
// First by name, then by age
const nameCompare = a.name.localeCompare(b.name);
if (nameCompare !== 0) return nameCompare;
return a.age - b.age;
});
10. The slice() and splice() Methods: Extract and Modify
One letter apart, very different behavior. slice() extracts a portion without modifying the original, while splice() modifies the array in place.
| Feature | slice() | splice() |
|---|---|---|
| Mutates Original | No ✓ | Yes ⚠️ |
| Purpose | Copy portion of array | Add/remove elements |
| Returns | New array | Removed elements |
| Parameters | (start, end) | (start, deleteCount, ...items) |
// slice() - extract without modifying
const arr = [0, 1, 2, 3, 4, 5];
arr.slice(2); // [2, 3, 4, 5] - from index 2 to end
arr.slice(1, 4); // [1, 2, 3] - from index 1 to 3 (end exclusive)
arr.slice(-2); // [4, 5] - last 2 elements
arr.slice(1, -1); // [1, 2, 3, 4] - exclude first and last
// Original unchanged
console.log(arr); // [0, 1, 2, 3, 4, 5]
// Clone array
const clone = arr.slice(); // or [...arr]
// splice() - modify in place
const letters = ['a', 'b', 'c', 'd', 'e'];
// Remove elements
const removed = letters.splice(2, 2); // Start at 2, remove 2
// removed: ['c', 'd']
// letters: ['a', 'b', 'e']
// Insert elements
letters.splice(2, 0, 'x', 'y'); // Start at 2, remove 0, insert 'x','y'
// letters: ['a', 'b', 'x', 'y', 'e']
// Replace elements
letters.splice(1, 2, 'NEW'); // Start at 1, remove 2, insert 'NEW'
// letters: ['a', 'NEW', 'y', 'e']
// Immutable alternative: toSpliced() (ES2023)
const original = [1, 2, 3, 4, 5];
const modified = original.toSpliced(2, 1, 'three');
// original: [1, 2, 3, 4, 5] (unchanged)
// modified: [1, 2, 'three', 4, 5]
Method Chaining: Building Data Pipelines
Because most of these methods return arrays, you can chain them, and each step reads like one line of a spec: keep these, sort by that, reshape into this. It's the main reason I prefer them over hand-written loops for this kind of code.
Each step in the chain does one job and hands its result to the next
// Real-world example: E-commerce product processing
const products = [
{ id: 1, name: 'Laptop', price: 999, category: 'Electronics', rating: 4.5, inStock: true },
{ id: 2, name: 'Phone', price: 699, category: 'Electronics', rating: 4.8, inStock: true },
{ id: 3, name: 'Headphones', price: 199, category: 'Electronics', rating: 4.2, inStock: false },
{ id: 4, name: 'Desk Chair', price: 299, category: 'Furniture', rating: 4.0, inStock: true },
{ id: 5, name: 'Monitor', price: 449, category: 'Electronics', rating: 4.6, inStock: true }
];
// Get top-rated in-stock electronics, formatted for display
const featuredProducts = products
.filter(p => p.category === 'Electronics') // Only electronics
.filter(p => p.inStock) // Must be in stock
.filter(p => p.rating >= 4.5) // High rated
.sort((a, b) => b.rating - a.rating) // Best first
.map(p => ({ // Transform
id: p.id,
displayName: `${p.name} ⭐${p.rating}`,
formattedPrice: `$${p.price.toLocaleString()}`
}));
// Result:
// [
// { id: 2, displayName: 'Phone ⭐4.8', formattedPrice: '$699' },
// { id: 5, displayName: 'Monitor ⭐4.6', formattedPrice: '$449' },
// { id: 1, displayName: 'Laptop ⭐4.5', formattedPrice: '$999' }
// ]
// Calculate total value of in-stock items
const inventoryValue = products
.filter(p => p.inStock)
.reduce((total, p) => total + p.price, 0);
// Result: 2446
// Group and summarize
const categorySummary = products
.filter(p => p.inStock)
.reduce((acc, product) => {
const cat = product.category;
if (!acc[cat]) {
acc[cat] = { count: 0, totalValue: 0, avgRating: 0, ratings: [] };
}
acc[cat].count++;
acc[cat].totalValue += product.price;
acc[cat].ratings.push(product.rating);
acc[cat].avgRating = acc[cat].ratings.reduce((a, b) => a + b) / acc[cat].ratings.length;
return acc;
}, {});
Quick Reference Cheat Sheet
Array Methods at a Glance
ES2023+ New Array Methods
ES2023 added non-mutating versions of the methods that used to change arrays in place, plus a couple of search helpers. They're in all current major browsers and Node 20+, but check your targets before relying on them.
// Immutable versions (ES2023) - original unchanged!
const arr = [3, 1, 2];
arr.toSorted((a, b) => a - b); // [1, 2, 3] - sort without mutating
arr.toReversed(); // [2, 1, 3] - reverse without mutating
arr.toSpliced(1, 1, 'new'); // [3, 'new', 2] - splice without mutating
arr.with(1, 'replaced'); // [3, 'replaced', 2] - replace at index
// findLast() and findLastIndex() - search from end
const nums = [1, 2, 3, 4, 3, 2, 1];
nums.findLast(n => n > 2); // 3 (the second one)
nums.findLastIndex(n => n > 2); // 4
// Array.fromAsync() - create array from async iterables
const asyncArray = await Array.fromAsync(asyncGenerator());
// Check these with your target browser support!
Performance Considerations
Performance Habits
- Remember each chained step creates a new array
- Use
find()overfilter()[0]when you need one element - Use
some()overfilter().length > 0for existence checks - Consider
for...ofloops for very large arrays - Use
reduce()to combine multiple operations - Avoid creating intermediate arrays when possible
// ❌ Inefficient: creates 2 intermediate arrays
const result = hugeArray
.filter(x => x > 10)
.filter(x => x < 100)
.map(x => x * 2);
// ✓ Better: single reduce
const result = hugeArray.reduce((acc, x) => {
if (x > 10 && x < 100) {
acc.push(x * 2);
}
return acc;
}, []);
// ✓ Or combine filters
const result = hugeArray
.filter(x => x > 10 && x < 100)
.map(x => x * 2);
When I'd use array methods (and when I'd reach for a loop)
For most application code (API responses, form data, lists you render) I default to filter, map, and friends. The arrays are small, the intent is obvious, and not mutating anything saves real debugging time.
I switch to a plain for or for...of loop when:
- I need to stop early with something other than a single match. You can't
breakout offorEachormap. - There's
awaitinside the loop and the steps have to run in order. - The array is large and the code is hot, like processing a big CSV import or running every animation frame. Chains allocate a new array per step; a single loop doesn't. Profile first, though. For a few hundred items it doesn't matter.
- A
reduce()is getting clever. If I have to read it twice to understand the accumulator, a loop with a named variable is usually clearer for whoever maintains it.
And whichever you pick, know which methods mutate. sort, reverse, and splice change the original array; forgetting that is one of the most common array bugs there is.