🔄 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 withflatMap() - 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.
Their shared advantages:
- Readability — the callback names the operation (double, keep-evens, total).
- Immutability —
map,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() 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.
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:
- Start from
recordsbelow — each has astudent,subject, andscore. - Use
reduce()to group scores by student. - For each student, compute the average (with
reduce) and a letter grade. - Return an array sorted by average, highest first, using
map()thensort().
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:
mapto transform,filterto select,reduceto aggregate. - Always seed
reduce()with an initial value. - Keep callbacks pure — no mutating outside state — so chains stay predictable.
- Use
some/everyfor boolean questions andfindfor single lookups.
⚠️ Don't
- Don't use
map()when you ignore its result — useforEach()orfor...of. - Don't expect to
breakout offorEach()/map(); usefor...oforsome(). - 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.