Skip to content

Arrays and Iteration Methods

beginner18 min read

Arrays Are Everywhere

If you write JavaScript, you work with arrays. Rendering a list of users? Array. Filtering search results? Array. Calculating a total? Array. They're the most commonly used data structure in frontend development, and knowing your way around array methods is what separates developers who write clean, readable code from those who litter everything with for loops.

Here's the thing most beginners miss: JavaScript arrays aren't just lists. They're objects with special behavior. Their indices are property keys (strings, technically), and they have a magic length property that auto-updates. Understanding this foundation makes everything else click.

Mental Model

Think of a JavaScript array as a numbered shelf. Each slot has an index (starting at 0), and you can put any value in any slot. Some methods rearrange the shelf in place (mutating methods like push, sort, splice) while others build you a brand new shelf from the original (non-mutating methods like map, filter, slice). The original shelf stays untouched with non-mutating methods. This distinction — does it change the original or create a new one? — is the single most important thing to understand about array methods.

Creating Arrays

There are several ways to create arrays. You'll use the first one 95% of the time.

Array Literals

The simplest and most common way:

const fruits = ["apple", "banana", "cherry"];
const numbers = [1, 2, 3, 4, 5];
const mixed = [1, "hello", true, null, { name: "Bob" }];
const empty = [];

Array.from() — Convert Anything Iterable

Array.from() creates an array from any iterable or array-like object. It also accepts a mapping function as a second argument:

// From a string
Array.from("hello"); // ["h", "e", "l", "l", "o"]

// From a Set
Array.from(new Set([1, 2, 2, 3])); // [1, 2, 3]

// With a mapping function (second argument)
Array.from({ length: 5 }, (_, i) => i * 2); // [0, 2, 4, 6, 8]

// From NodeList (common in DOM work)
Array.from(document.querySelectorAll("li"));

Array.of() — Create from Arguments

Unlike the Array constructor, Array.of() always creates an array from its arguments without special-casing single numbers:

Array.of(5);       // [5] — an array with one element
Array(5);          // [,,,,] — an empty array with length 5 (confusing!)
Array.of(1, 2, 3); // [1, 2, 3]

Spread into a New Array

The spread operator (...) copies elements from one array into another:

const original = [1, 2, 3];
const copy = [...original];       // [1, 2, 3] — shallow copy
const combined = [...original, 4, 5]; // [1, 2, 3, 4, 5]
const merged = [...[1, 2], ...[3, 4]]; // [1, 2, 3, 4]
Shallow copies only

Both Array.from() and spread create shallow copies. If your array contains objects, the new array holds references to the same objects, not clones. Mutating an object inside the copy mutates it in the original too.

Quiz
What does Array.of(3) return?

Accessing and Modifying Elements

Bracket Notation

The classic way to access elements by index:

const colors = ["red", "green", "blue"];
colors[0];  // "red"
colors[2];  // "blue"
colors[5];  // undefined — no error, just undefined
colors[-1]; // undefined — negative indices don't work with brackets

The at() Method

at() supports negative indices, making it easy to access elements from the end:

const colors = ["red", "green", "blue"];
colors.at(0);  // "red"
colors.at(-1); // "blue" — last element
colors.at(-2); // "green" — second to last

The length Property

length is always one more than the highest index. It's also writable — you can truncate an array by setting it:

const arr = [1, 2, 3, 4, 5];
arr.length; // 5

arr.length = 3;
arr; // [1, 2, 3] — elements 4 and 5 are gone permanently

arr.length = 5;
arr; // [1, 2, 3, undefined, undefined] — doesn't restore them
Common Trap

Setting length to a smaller value permanently deletes elements. There's no undo. This is a mutation, and it catches people off guard because length looks like a read-only property.

Mutating Methods — They Change the Original

These methods modify the array in place and return something (not always the modified array). This is where bugs hide if you're not careful.

Adding and Removing Elements

const arr = [1, 2, 3];

// push — add to end, returns new length
arr.push(4);      // returns 4 (new length), arr is [1, 2, 3, 4]
arr.push(5, 6);   // returns 6, arr is [1, 2, 3, 4, 5, 6]

// pop — remove from end, returns removed element
arr.pop();        // returns 6, arr is [1, 2, 3, 4, 5]

// unshift — add to start, returns new length
arr.unshift(0);   // returns 6, arr is [0, 1, 2, 3, 4, 5]

// shift — remove from start, returns removed element
arr.shift();      // returns 0, arr is [1, 2, 3, 4, 5]

splice() — The Swiss Army Knife

splice(startIndex, deleteCount, ...itemsToInsert) can add, remove, or replace elements. It returns an array of removed elements.

const arr = ["a", "b", "c", "d", "e"];

// Remove 2 elements starting at index 1
arr.splice(1, 2);     // returns ["b", "c"], arr is ["a", "d", "e"]

// Insert without removing (deleteCount = 0)
arr.splice(1, 0, "x"); // returns [], arr is ["a", "x", "d", "e"]

// Replace: remove 1, insert 2
arr.splice(2, 1, "y", "z"); // returns ["d"], arr is ["a", "x", "y", "z", "e"]

sort() — Sorting In Place

sort() sorts the array in place and returns the sorted array. Without a comparator function, it converts elements to strings and sorts lexicographically:

const words = ["banana", "apple", "cherry"];
words.sort(); // ["apple", "banana", "cherry"] — works fine for strings

const nums = [10, 9, 2, 21, 3];
nums.sort(); // [10, 2, 21, 3, 9] — WRONG! Sorted as strings: "10" < "2"

Always pass a comparator for numbers:

const nums = [10, 9, 2, 21, 3];
nums.sort((a, b) => a - b); // [2, 3, 9, 10, 21] — ascending
nums.sort((a, b) => b - a); // [21, 10, 9, 3, 2] — descending

reverse() and fill()

const arr = [1, 2, 3];
arr.reverse(); // [3, 2, 1] — mutates in place

const slots = new Array(5);
slots.fill(0);    // [0, 0, 0, 0, 0]
slots.fill(7, 1, 3); // [0, 7, 7, 0, 0] — fill with 7 from index 1 to 3
Quiz
What does [5, 1, 10, 2].sort() produce?

Non-Mutating Methods — They Return New Values

These methods never touch the original array. They return a new array, a value, or a boolean. This makes them safe and predictable — which is why modern JavaScript strongly favors them.

map() — Transform Every Element

map() calls your function on each element and returns a new array of the results. Same length in, same length out.

const nums = [1, 2, 3, 4];
const doubled = nums.map(n => n * 2); // [2, 4, 6, 8]

const users = [{ name: "Alice" }, { name: "Bob" }];
const names = users.map(u => u.name); // ["Alice", "Bob"]

filter() — Keep Only What Passes

filter() calls your function on each element and returns a new array containing only the elements where your function returned a truthy value.

const nums = [1, 2, 3, 4, 5, 6];
const evens = nums.filter(n => n % 2 === 0); // [2, 4, 6]

const users = [
  { name: "Alice", active: true },
  { name: "Bob", active: false },
  { name: "Charlie", active: true },
];
const active = users.filter(u => u.active);
// [{ name: "Alice", active: true }, { name: "Charlie", active: true }]

reduce() — Collapse Into a Single Value

reduce() iterates through the array, passing an accumulator and the current element to your function. It returns a single value.

const nums = [1, 2, 3, 4, 5];

// Sum
const total = nums.reduce((sum, n) => sum + n, 0); // 15

// Find max
const max = nums.reduce((m, n) => n > m ? n : m, -Infinity); // 5

// Group by property
const people = [
  { name: "Alice", dept: "eng" },
  { name: "Bob", dept: "design" },
  { name: "Charlie", dept: "eng" },
];
const byDept = people.reduce((groups, person) => {
  const key = person.dept;
  groups[key] = groups[key] || [];
  groups[key].push(person);
  return groups;
}, {});
// { eng: [Alice, Charlie], design: [Bob] }
Always provide an initial value

Calling reduce() without an initial value (second argument) uses the first element as the initial accumulator and starts iteration from the second element. On an empty array with no initial value, it throws a TypeError. Always pass the initial value to be safe.

find() and findIndex() — Get the First Match

find() returns the first element that passes the test (or undefined). findIndex() returns its index (or -1).

const users = [
  { id: 1, name: "Alice" },
  { id: 2, name: "Bob" },
  { id: 3, name: "Charlie" },
];

users.find(u => u.id === 2);      // { id: 2, name: "Bob" }
users.find(u => u.id === 99);     // undefined
users.findIndex(u => u.id === 2); // 1
users.findIndex(u => u.id === 99); // -1

some() and every() — Boolean Checks

some() returns true if at least one element passes. every() returns true only if all elements pass. Both short-circuit — they stop iterating as soon as the answer is known.

const nums = [1, 3, 5, 7, 8];

nums.some(n => n % 2 === 0);  // true — 8 is even
nums.every(n => n % 2 === 0); // false — most are odd

nums.some(n => n > 100);      // false — none are > 100
nums.every(n => n > 0);       // true — all are positive

includes() — Simple Existence Check

includes() checks if a value is in the array using SameValueZero comparison (like === but NaN === NaN is true):

const arr = [1, 2, 3, NaN];
arr.includes(2);   // true
arr.includes(NaN); // true — unlike indexOf which can't find NaN
arr.indexOf(NaN);  // -1 — indexOf uses ===, and NaN !== NaN

flat() and flatMap() — Flatten Nested Arrays

const nested = [[1, 2], [3, 4], [5]];
nested.flat();     // [1, 2, 3, 4, 5]

const deep = [1, [2, [3, [4]]]];
deep.flat();       // [1, 2, [3, [4]]] — one level by default
deep.flat(Infinity); // [1, 2, 3, 4] — flatten all levels

// flatMap = map then flat(1) — but more efficient
const sentences = ["hello world", "goodbye moon"];
sentences.flatMap(s => s.split(" "));
// ["hello", "world", "goodbye", "moon"]

slice() and concat() — Copy and Combine

const arr = [1, 2, 3, 4, 5];

// slice(start, end) — end is exclusive
arr.slice(1, 3);  // [2, 3]
arr.slice(-2);    // [4, 5] — last two
arr.slice();      // [1, 2, 3, 4, 5] — shallow copy

// concat — combine arrays
arr.concat([6, 7]);        // [1, 2, 3, 4, 5, 6, 7]
arr.concat([6], [7], [8]); // [1, 2, 3, 4, 5, 6, 7, 8]
Quiz
What does [1, 2, 3].map(n => n > 1) return?

Mutating vs Non-Mutating — The Complete Picture

This is the distinction that matters most in practice. Mutating methods change your original data, which can cause bugs in React (where state should be immutable), in shared data structures, and anywhere a function shouldn't have side effects.

MethodMutates?Returns
push()YesNew length
pop()YesRemoved element
shift()YesRemoved element
unshift()YesNew length
splice()YesArray of removed elements
sort()YesThe sorted array (same reference)
reverse()YesThe reversed array (same reference)
fill()YesThe modified array (same reference)
map()NoNew array of transformed elements
filter()NoNew array of matching elements
reduce()NoSingle accumulated value
find()NoFirst matching element or undefined
findIndex()NoIndex of first match or -1
some()NoBoolean
every()NoBoolean
includes()NoBoolean
flat()NoNew flattened array
flatMap()NoNew mapped and flattened array
slice()NoNew array (subset or copy)
concat()NoNew combined array
New immutable alternatives in ES2023

ES2023 added non-mutating versions of the most common mutating methods:

  • toSorted() — like sort() but returns a new array
  • toReversed() — like reverse() but returns a new array
  • toSpliced() — like splice() but returns a new array
  • with(index, value) — like arr[index] = value but returns a new array
const nums = [3, 1, 2];
const sorted = nums.toSorted((a, b) => a - b); // [1, 2, 3]
nums; // [3, 1, 2] — untouched

const arr = ["a", "b", "c"];
const updated = arr.with(1, "x"); // ["a", "x", "c"]
arr; // ["a", "b", "c"] — untouched

These are supported in all modern browsers and Node.js 20+. They're especially useful in React where you need immutable state updates.

Method Chaining

Because non-mutating methods return arrays, you can chain them together to build data transformation pipelines:

const orders = [
  { product: "Laptop", price: 999, quantity: 1 },
  { product: "Mouse", price: 25, quantity: 3 },
  { product: "Keyboard", price: 75, quantity: 0 },
  { product: "Monitor", price: 450, quantity: 2 },
  { product: "Cable", price: 10, quantity: 5 },
];

const expensiveInStock = orders
  .filter(o => o.quantity > 0)        // remove out-of-stock
  .filter(o => o.price > 20)          // remove cheap items
  .map(o => ({                        // transform shape
    name: o.product,
    total: o.price * o.quantity,
  }))
  .sort((a, b) => b.total - a.total); // sort by total descending

// [{ name: "Laptop", total: 999 }, { name: "Monitor", total: 900 }, ...]

Each step is readable, testable, and does one thing. Compare that to a for loop doing all four operations in one tangled block.

Performance note on chaining

Each chained method creates an intermediate array. For most use cases (arrays under a few thousand elements), this is totally fine. If you're processing millions of items, a single reduce() or a for loop can avoid the intermediate allocations. Profile first, optimize later.

When to Use Which Method

Choosing the right method makes your intent clear. Here's the decision framework:

Need to transform every element? Use map().

Need to keep only some elements? Use filter().

Need to collapse an array into a single value? (sum, object, string) Use reduce().

Need to find one specific element? Use find() (for the element) or findIndex() (for the index).

Need to check if something exists? Use includes() (for a value) or some() (for a condition).

Need to verify all elements pass a test? Use every().

Need to flatten nested arrays? Use flat() or flatMap() if you're also mapping.

Need to add/remove elements at specific positions? Use splice() (mutating) or toSpliced() (non-mutating).

Need a portion of an array without modifying it? Use slice().

Quiz
You have an array of users and want to get only the names of users who are older than 18. Which chain is correct?

Common Pitfalls

sort() Without a Comparator

This is the trap that gets everyone at least once:

[10, 9, 2, 21, 3].sort();
// [10, 2, 21, 3, 9] — lexicographic string sort!

[10, 9, 2, 21, 3].sort((a, b) => a - b);
// [2, 3, 9, 10, 21] — numeric sort

Sparse Arrays

Holes in arrays behave unpredictably across methods:

const sparse = [1, , 3]; // hole at index 1
sparse.length;           // 3
sparse[1];               // undefined

sparse.map(x => x * 2);     // [2, empty, 6] — skips the hole
sparse.forEach(x => console.log(x)); // logs 1, 3 — skips the hole
[...sparse];                 // [1, undefined, 3] — fills the hole
Array.from(sparse);          // [1, undefined, 3] — fills the hole
Common Trap

Sparse arrays (arrays with holes) behave differently depending on the method you use. map(), forEach(), filter(), and reduce() all skip holes entirely. But spread and Array.from() convert holes to undefined. The safest strategy? Avoid sparse arrays altogether.

Comparing Arrays

You can't compare arrays with === — it checks reference identity, not contents:

[1, 2, 3] === [1, 2, 3]; // false — two different objects
[1, 2, 3] == [1, 2, 3];  // false — still different objects

// To compare contents:
const a = [1, 2, 3];
const b = [1, 2, 3];
a.length === b.length && a.every((val, i) => val === b[i]); // true
What developers doWhat they should do
Using sort() on numbers without a comparator
sort() converts elements to strings by default, so 10 comes before 2 because the string '10' is less than '2' lexicographically.
Always pass (a, b) => a - b for numeric sorting
Expecting map() to filter elements
map() always returns an array with the same length as the input. It transforms each element, it doesn't remove any.
Use filter() to remove elements, map() to transform them
Calling reduce() on an empty array without an initial value
reduce() with no initial value on an empty array throws TypeError. The initial value also makes the code clearer about what you're building.
Always provide the second argument to reduce()
Comparing arrays with === or ==
=== compares object references, not contents. Two arrays with identical values are still different objects in memory.
Use every() to compare element by element, or JSON.stringify() for simple cases
Forgetting that sort() and reverse() mutate the original
These methods modify the array in place and return the same reference. If you're using the original elsewhere (especially in React state), you'll get unexpected behavior.
Use toSorted() and toReversed() for non-mutating versions, or spread first: [...arr].sort()
Key Rules
  1. 1Mutating methods change the original array — non-mutating methods return a new one
  2. 2Always pass a comparator to sort() when sorting numbers
  3. 3filter() selects, map() transforms, reduce() accumulates — don't mix them up
  4. 4find() returns the element or undefined, findIndex() returns the index or -1
  5. 5includes() handles NaN correctly, indexOf() does not
  6. 6Chain filter before map — filter after map loses the data you need to filter on
  7. 7reduce() always needs an initial value for safety and clarity
Quiz
What does [1, 2, 3].filter(n => n > 1).reduce((sum, n) => sum + n, 0) return?
Challenge:

Try to solve it before peeking at the answer.

Write a function that takes an array of objects with name and score properties and returns a comma-separated string of names for everyone who scored above 80, sorted alphabetically.

function topScorers(students) {
  // Your code here
}

// Example:
topScorers([
  { name: "Charlie", score: 92 },
  { name: "Alice", score: 85 },
  { name: "Bob", score: 70 },
  { name: "Diana", score: 95 },
]);
// Expected: "Alice, Charlie, Diana"