Skip to main content

🔄 Iteration Methods (map, filter, reduce)

The iteration methods are where JavaScript array code stops looking like machinery and starts reading like a sentence. Instead of describing how to loop, you declare what you want: transform each item, keep the ones that match, or boil the whole list down to one value.

🎯 Learning Objectives

By the end of this lesson, you will be able to:

  • Run side effects over an array with forEach() and know its limits
  • Transform every element into a new array with map()
  • Select elements that pass a test with filter()
  • Aggregate an array into a single value — sum, object, or group — with reduce()
  • Test collections with some()/every() and flatten with flatMap()
  • Combine methods with chaining and reason about the performance trade-offs

Estimated Time: 40–50 minutes  •  Difficulty: Intermediate

Hands-on: Build a data pipeline that turns a raw grade list into a per-student report.

In This Lesson

Declarative vs. Imperative

A traditional for loop is imperative — you spell out the counter, the bound, the increment, and the indexing. Iteration methods are declarative — you hand a small function to the array and let it do the walking. The result is code that expresses intent, not mechanics.

graph TD A[Iteration methods] --> B["forEach — do something per item"] A --> C["map — transform → new array"] A --> D["filter — keep matches → new array"] A --> E["reduce — accumulate → one value"] A --> F["some / every — test → boolean"] A --> G["flatMap — map then flatten"]

Their shared advantages:

  • Readability — the callback names the operation (double, keep-evens, total).
  • Immutabilitymap, filter, and friends return new arrays and leave the source alone.
  • Chainability — because each returns an array, you can pipe one into the next.

forEach — Side Effects

forEach() runs a callback once per element. It's the declarative cousin of a for loop, meant for side effects (logging, updating the DOM) — not for producing a value.

const fruits = ["Apple", "Banana", "Cherry"];

fruits.forEach((fruit, index) => {
  console.log(`${index}: ${fruit}`);
});
// 0: Apple
// 1: Banana
// 2: Cherry

⚠️ Two things forEach can't do

It returns nothing (undefined), so const x = arr.forEach(...) is always undefined — reach for map() if you want a result. You can't break out early: break is a syntax error and return only skips the current item. When you need early exit, use a for...of loop or some()/find().

map — Transform

map() builds a new array by running each element through your function. One in, one out — the output array always has the same length as the input.

map transforms each element into a new array The array 1,2,3,4 passes through a doubling function and becomes 2,4,6,8. [1, 2, 3, 4] x => x * 2 [2, 4, 6, 8] original new array
Figure 1 — map() is a transformation conveyor: every element goes through the function and a new array comes out.
const numbers = [1, 2, 3, 4, 5];
const doubled = numbers.map(n => n * 2);
console.log(doubled);  // [2, 4, 6, 8, 10]
console.log(numbers);  // [1, 2, 3, 4, 5] — original untouched

// Transforming objects — a very common real-world job
const users = [
  { id: 1, name: "John", age: 30 },
  { id: 2, name: "Jane", age: 25 }
];
const names = users.map(u => u.name);                 // ["John", "Jane"]
const cards = users.map(u => ({ id: u.id, label: u.name.toUpperCase() }));

💡 map or forEach?

If you want a new array of results, use map(). If you just want to do something for each element and don't need a return value, use forEach(). Using map() purely for side effects (ignoring its return) is a smell.

filter — Select

filter() returns a new array containing only the elements for which your callback returns a truthy value. Think of it as a sieve: matches pass through, everything else is left behind.

const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

const evens = numbers.filter(n => n % 2 === 0);   // [2, 4, 6, 8, 10]
const big   = numbers.filter(n => n > 5);         // [6, 7, 8, 9, 10]

// Filtering objects with combined conditions
const products = [
  { name: "Laptop", price: 999, inStock: true },
  { name: "Tablet", price: 399, inStock: false },
  { name: "Headphones", price: 199, inStock: true }
];
const affordableInStock = products.filter(p => p.price < 500 && p.inStock);
console.log(affordableInStock); // [{ name: "Headphones", ... }]

📖 Remove falsy values in one line

const clean = messy.filter(Boolean); drops 0, "", null, undefined, NaN, and false — a handy idiom for cleaning up a list before processing it.

reduce — Aggregate

reduce() is the most powerful and most misunderstood of the group. It walks the array carrying an accumulator from one step to the next, folding everything into a single result — a number, an object, even another array.

flowchart LR S["start: 0"] --> A["0 + 1 = 1"] A --> B["1 + 2 = 3"] B --> C["3 + 3 = 6"] C --> D["6 + 4 = 10"] D --> E["10 + 5 = 15"] E --> R["result: 15"]
const numbers = [1, 2, 3, 4, 5];

// (accumulator, current) => nextAccumulator ; second arg is the INITIAL value
const sum = numbers.reduce((acc, n) => acc + n, 0);
console.log(sum); // 15

⚠️ Always pass an initial value

Calling reduce() on an empty array with no initial value throws TypeError: Reduce of empty array with no initial value. Supplying the seed (0, {}, []) also makes the accumulator's type obvious and the first iteration consistent.

Beyond sums: counting and grouping

// Frequency counter — accumulator is an object
const fruits = ["apple", "banana", "apple", "orange", "banana", "apple"];
const counts = fruits.reduce((acc, fruit) => {
  acc[fruit] = (acc[fruit] || 0) + 1;
  return acc;
}, {});
console.log(counts); // { apple: 3, banana: 2, orange: 1 }

// Group objects by a property
const people = [
  { name: "Alice", dept: "Engineering" },
  { name: "Bob",   dept: "Marketing" },
  { name: "Eve",   dept: "Engineering" }
];
const byDept = people.reduce((acc, person) => {
  (acc[person.dept] ||= []).push(person);
  return acc;
}, {});
// { Engineering: [Alice, Eve], Marketing: [Bob] }

✅ reduce is a superset

You can express map and filter in terms of reduce (push transformed/kept items into an array accumulator). You usually shouldn't — the dedicated methods are clearer — but it shows why reduce is the fundamental building block.

some, every & flatMap

some() and every() — boolean tests

some() returns true if at least one element passes; every() returns true only if all do. Both short-circuit — they stop as soon as the answer is settled.

const numbers = [1, 2, 3, 4, 5];
console.log(numbers.some(n => n % 2 === 0));  // true  — 2 is even
console.log(numbers.every(n => n > 0));       // true  — all positive

// Form validation reads naturally
const fields = [
  { name: "username", valid: true },
  { name: "password", valid: false }
];
const formValid = fields.every(f => f.valid);
console.log(formValid ? "Submit" : "Fix errors"); // "Fix errors"

flatMap() — map then flatten one level

const sentences = ["Hello world", "How are you"];
const words = sentences.flatMap(s => s.split(" "));
console.log(words); // ["Hello", "world", "How", "are", "you"]

// Expand nested data — flatMap shines when each item yields 0..n results
const userPosts = [
  { user: "Alice", posts: ["Post 1", "Post 2"] },
  { user: "Bob",   posts: ["Post 3"] },
  { user: "Carol", posts: [] }
];
const feed = userPosts.flatMap(u =>
  u.posts.map(p => ({ author: u.user, content: p }))
);
// [{author:"Alice",content:"Post 1"}, {author:"Alice",content:"Post 2"}, {author:"Bob",content:"Post 3"}]

Method Chaining

Because map and filter each return an array, you can chain them into a readable pipeline that reads top-to-bottom like a description of the query.

const products = [
  { name: "Laptop", price: 999, inStock: true,  category: "Electronics" },
  { name: "Phone",  price: 699, inStock: true,  category: "Electronics" },
  { name: "Tablet", price: 399, inStock: false, category: "Electronics" },
  { name: "Headphones", price: 199, inStock: true, category: "Electronics" }
];

// Names of the top 2 most expensive in-stock electronics
const topPicks = products
  .filter(p => p.category === "Electronics" && p.inStock)
  .sort((a, b) => b.price - a.price)
  .slice(0, 2)
  .map(p => p.name);

console.log(topPicks); // ["Laptop", "Phone"]

💡 Chaining vs. performance

Each link in a chain is a full pass over the data, and map/filter allocate intermediate arrays. For everyday list sizes this is completely fine — favor clarity. Only when profiling flags a hot path over a very large array should you consolidate passes (e.g. a single reduce) or drop to a for loop. And remember: to find one element, use find(), not filter(...)[0], so you can stop early.

Hands-on Exercise

🏋️ Grades to Report Cards

Objective: Chain iteration methods to turn a flat list of scores into a per-student summary.

Instructions:

  1. Start from records below — each has a student, subject, and score.
  2. Use reduce() to group scores by student.
  3. For each student, compute the average (with reduce) and a letter grade.
  4. Return an array sorted by average, highest first, using map() then sort().
const records = [
  { student: "Ana", subject: "Math",    score: 92 },
  { student: "Ana", subject: "Science", score: 88 },
  { student: "Ben", subject: "Math",    score: 74 },
  { student: "Ben", subject: "Science", score: 80 },
  { student: "Cy",  subject: "Math",    score: 61 }
];
💡 Hint

Group first: records.reduce((acc, r) => { (acc[r.student] ||= []).push(r.score); return acc; }, {}). Then Object.entries(grouped).map(...) to compute averages. A letter helper: avg >= 90 ? "A" : avg >= 80 ? "B" : avg >= 70 ? "C" : "D".

✅ Example solution
const grouped = records.reduce((acc, r) => {
  (acc[r.student] ||= []).push(r.score);
  return acc;
}, {});

const letter = avg =>
  avg >= 90 ? "A" : avg >= 80 ? "B" : avg >= 70 ? "C" : "D";

const report = Object.entries(grouped)
  .map(([student, scores]) => {
    const avg = scores.reduce((s, n) => s + n, 0) / scores.length;
    return { student, average: Math.round(avg), grade: letter(avg) };
  })
  .sort((a, b) => b.average - a.average);

console.log(report);
// [ { student: "Ana", average: 90, grade: "A" },
//   { student: "Ben", average: 77, grade: "C" },
//   { student: "Cy",  average: 61, grade: "D" } ]

🎯 Quick Quiz

Question 1: You want a new array with each number tripled. Which method fits?

Question 2: Why does [].reduce((a, b) => a + b) throw?

Question 3: Which method returns true only if every element passes the test?

Best Practices

✅ Do

  • Choose the method that names your intent: map to transform, filter to select, reduce to aggregate.
  • Always seed reduce() with an initial value.
  • Keep callbacks pure — no mutating outside state — so chains stay predictable.
  • Use some/every for boolean questions and find for single lookups.

⚠️ Don't

  • Don't use map() when you ignore its result — use forEach() or for...of.
  • Don't expect to break out of forEach()/map(); use for...of or some().
  • Don't cram unrelated logic into one giant reduce() when a short chain reads better.
  • Don't reach for filter(...)[0]find() is clearer and stops early.

Summary & Quiz

🎉 Key Takeaways

  • forEach() runs side effects and returns nothing; it can't break early.
  • map() transforms into a same-length new array; filter() selects a subset.
  • reduce() folds an array into one value — always give it an initial value.
  • some()/every() answer boolean questions; flatMap() maps then flattens a level.
  • Chaining keeps complex queries readable; each link is a pass, so favor clarity and profile before optimizing.

📚 Further Reading

🚀 What's Next?

You've written a lot of short arrow-function callbacks in this lesson. Next we look at arrow functions in depth — their concise syntax and, crucially, how they capture this lexically, which is what makes them so natural inside these iteration methods.

🎉 Excellent!

You can now transform, filter, and aggregate data the functional way. That's a superpower you'll use every day.