Skip to main content

πŸ”§ Array Methods: map, filter & reduce

You already know how to loop over an array with for. But there's a cleaner, more expressive way to work with collections β€” one that says what you want, not the bookkeeping of how to get it. Meet the functional trio that powers modern JavaScript: map transforms, filter selects, and reduce accumulates.

Week 2 · Day 1 (Monday: Arrays and Objects) · Lecture 3

🎯 Learning Objectives

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

  • Use map() to transform every element into a new array of the same length
  • Use filter() to keep only the elements that pass a test
  • Use reduce() to boil an array down to a single value (a sum, an object, anything)
  • Chain filter β†’ map β†’ reduce into readable data pipelines
  • Recognize that all three return new values and never mutate the original array
  • Pick the right tool β€” including find, some, and every β€” for the job

Estimated Time: 70 minutes

Practice: Convert temperatures, strip falsy values, rebuild map with reduce, and analyze a student dataset.

In This Lesson

Why Functional Methods?

Imagine a chef standing at a conveyor belt of ingredients. map transforms each ingredient β€” chopping every vegetable. filter keeps only some β€” picking out the ripe tomatoes. reduce combines everything into one result β€” simmering it all into a single pot of soup. Each takes a small function and applies it across the whole array for you.

graph LR A[Original Array] --> B{map} B --> C[New Array
same length] A --> D{filter} D --> E[New Array
fewer or equal] A --> F{reduce} F --> G[Single Value]

Compared to a hand-written for loop, these methods are declarative: they describe the goal, hide the loop counter and index arithmetic, and β€” crucially β€” return a new value without ever mutating the original. That immutability makes your code easier to reason about and is the norm in modern JavaScript and React.

map transforms each element, filter selects some, reduce collapses to one value map 123 β†’ Γ—2 β†’ 246 filter 123 β†’ even β†’ 2 reduce 123 β†’ sum β†’ 6
The same input [1, 2, 3] through each method: map keeps the count, filter can shrink it, reduce collapses it to one value.

map() β€” Transform Every Element

The map() method creates a new array by running a function on every element of the original. The result is always the same length as the input β€” one output per input. Think of it as a magic wand that reshapes each item.

Syntax & simple examples

// array.map(callback(currentValue, index, array))  β†’  new array

// Double every number
const numbers = [1, 2, 3, 4, 5];
const doubled = numbers.map(num => num * 2);
console.log(doubled);   // [2, 4, 6, 8, 10]
console.log(numbers);   // [1, 2, 3, 4, 5] β€” original untouched

// Uppercase every string
const fruits = ['apple', 'banana', 'orange'];
console.log(fruits.map(fruit => fruit.toUpperCase())); // ['APPLE', 'BANANA', 'ORANGE']

// Pull one property out of each object
const users = [
    { id: 1, name: 'John', age: 30 },
    { id: 2, name: 'Jane', age: 25 },
    { id: 3, name: 'Bob',  age: 35 }
];
console.log(users.map(user => user.name)); // ['John', 'Jane', 'Bob']

Real-world: reshape API data for the UI

// Server sends snake_case; the UI wants a tidy shape
const apiResponse = [
    { user_id: 1, first_name: 'John', last_name: 'Doe',   email_address: 'john@example.com' },
    { user_id: 2, first_name: 'Jane', last_name: 'Smith', email_address: 'jane@example.com' }
];

const uiData = apiResponse.map(user => ({
    id: user.user_id,
    fullName: `${user.first_name} ${user.last_name}`,
    email: user.email_address,
    initials: `${user.first_name[0]}${user.last_name[0]}`
}));

console.log(uiData[0]);
// { id: 1, fullName: 'John Doe', email: 'john@example.com', initials: 'JD' }

⚠️ Wrap object literals in parentheses

To return an object from an arrow function you must wrap it: user => ({ id: user.id }). Without the parentheses, { } is read as a function body, not an object β€” a classic map mistake.

filter() β€” Keep Only What Passes

The filter() method builds a new array containing only the elements for which your test function returns true. It's the bouncer at the door: elements that fail the check don't get in. The result is the same length or shorter.

Syntax & simple examples

// array.filter(callback(element, index, array))  β†’  new array
// Return true to KEEP the element, false to drop it

// Keep only even numbers
const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
console.log(numbers.filter(num => num % 2 === 0)); // [2, 4, 6, 8, 10]

// Keep words longer than 5 characters
const words = ['cat', 'elephant', 'dog', 'rhinoceros', 'bird'];
console.log(words.filter(word => word.length > 5)); // ['elephant', 'rhinoceros']

// Keep objects that pass a property test
const products = [
    { name: 'Laptop', price: 999, inStock: true },
    { name: 'Phone',  price: 599, inStock: false },
    { name: 'Tablet', price: 349, inStock: true }
];
console.log(products.filter(p => p.inStock)); // [Laptop, Tablet]

Real-world: multi-criteria search

const inventory = [
    { id: 1, name: 'Laptop', price: 999, category: 'Electronics', rating: 4.5 },
    { id: 2, name: 'Desk',   price: 199, category: 'Furniture',   rating: 4.0 },
    { id: 3, name: 'Phone',  price: 699, category: 'Electronics', rating: 4.8 },
    { id: 4, name: 'Chair',  price: 149, category: 'Furniture',   rating: 3.9 }
];

function filterProducts(minPrice, maxPrice, category, minRating) {
    return inventory.filter(product =>
        product.price   >= minPrice &&
        product.price   <= maxPrice &&
        product.category === category &&
        product.rating  >= minRating
    );
}

console.log(filterProducts(100, 500, 'Furniture', 3.5));
// [{ id: 2, name: 'Desk', ... }, { id: 4, name: 'Chair', ... }]

βœ… A neat trick: filter(Boolean)

Passing the Boolean function to filter drops every "falsy" value (0, '', null, undefined, NaN, false) in one stroke: [0, 1, '', 2, null, 3].filter(Boolean) gives [1, 2, 3].

reduce() β€” Accumulate to a Single Value

The reduce() method is the most powerful β€” and the most feared β€” of the three. It walks the array carrying an accumulator from one element to the next, and returns whatever the accumulator ends up as. That final value can be a number, a string, an object, or even another array. Picture a snowball rolling downhill, gathering snow as it goes.

Syntax & simple examples

// array.reduce(callback(accumulator, currentValue, index, array), initialValue)

// Sum all numbers (initial accumulator = 0)
const numbers = [1, 2, 3, 4, 5];
const sum = numbers.reduce((acc, num) => acc + num, 0);
console.log(sum); // 15

// Find the maximum (no initialValue β†’ starts from the first element)
const values = [10, 5, 25, 15, 30];
const max = values.reduce((acc, val) => (val > acc ? val : acc));
console.log(max); // 30

// Count occurrences β€” the accumulator is an OBJECT
const fruits = ['apple', 'banana', 'apple', 'orange', 'banana', 'apple'];
const fruitCount = fruits.reduce((acc, fruit) => {
    acc[fruit] = (acc[fruit] || 0) + 1;
    return acc;
}, {});
console.log(fruitCount); // { apple: 3, banana: 2, orange: 1 }

⚠️ Almost always pass an initialValue

The second argument to reduce is the accumulator's starting value. Omit it and reduce uses the first element as the seed β€” which breaks on an empty array (it throws). Passing 0, {}, or [] explicitly makes your intent clear and your code safe.

Advanced: group objects by a property

const people = [
    { name: 'Alice', age: 25, city: 'NYC' },
    { name: 'Bob',   age: 30, city: 'LA'  },
    { name: 'Carol', age: 25, city: 'NYC' },
    { name: 'Dave',  age: 30, city: 'LA'  }
];

const groupByAge = people.reduce((acc, person) => {
    const key = person.age;
    (acc[key] ||= []).push(person);   // create the bucket if needed, then add
    return acc;
}, {});

console.log(Object.keys(groupByAge)); // ['25', '30']

πŸ’‘ Modern shortcut: Object.groupBy

Grouping is so common that JavaScript added Object.groupBy(people, p => p.age) (ES2024). Where it's available it replaces the manual reduce above β€” but understanding the reduce version teaches you the underlying pattern.

Chaining Methods

Because map and filter each return a new array, you can chain them into an assembly line where each stage does one job. Read a chain top-to-bottom like a recipe.

const products = [
    { name: 'Laptop',  price: 999, category: 'Electronics', sold: 150 },
    { name: 'Phone',   price: 599, category: 'Electronics', sold: 300 },
    { name: 'Desk',    price: 199, category: 'Furniture',   sold: 80  },
    { name: 'Chair',   price: 149, category: 'Furniture',   sold: 120 },
    { name: 'Monitor', price: 299, category: 'Electronics', sold: 90  }
];

// Top 3 electronics by revenue, formatted for display
const topElectronics = products
    .filter(p => p.category === 'Electronics')          // 1. keep electronics
    .map(p => ({ ...p, revenue: p.price * p.sold }))    // 2. compute revenue
    .sort((a, b) => b.revenue - a.revenue)              // 3. highest first
    .slice(0, 3)                                        // 4. take top 3
    .map(p => `${p.name}: $${p.revenue.toLocaleString()}`); // 5. format

console.log(topElectronics);
// ['Phone: $179,700', 'Laptop: $149,850', 'Monitor: $26,910']

Combining reduce for a summary

const users = [
    { id: 1, name: 'John', orders: [{ amount: 50 }, { amount: 75 }] },
    { id: 2, name: 'Jane', orders: [{ amount: 100 }, { amount: 50 }, { amount: 25 }] },
    { id: 3, name: 'Bob',  orders: [{ amount: 200 }] }
];

const bigSpenders = users
    .map(user => ({
        name: user.name,
        totalOrders: user.orders.length,
        totalSpent: user.orders.reduce((sum, o) => sum + o.amount, 0)
    }))
    .filter(user => user.totalSpent > 100)
    .sort((a, b) => b.totalSpent - a.totalSpent)
    .map(user => ({
        name: user.name,
        averageOrderValue: (user.totalSpent / user.totalOrders).toFixed(2)
    }));

console.log(bigSpenders);
// [ { name: 'Jane', averageOrderValue: '58.33' }, { name: 'Bob', averageOrderValue: '200.00' } ]

Choosing the Right Method

Reaching for reduce when map or filter would do makes code harder to read. Match the method to the shape of the result you want.

You want…UseReturns
A new array, one item per inputmap()Array (same length)
A subset that passes a testfilter()Array (≀ length)
One combined valuereduce()Any single value
The first matching elementfind()Element or undefined
"Does at least one match?"some()Boolean
"Do all of them match?"every()Boolean
const products = [
    { name: 'Laptop', price: 999, inStock: true },
    { name: 'Phone',  price: 599, inStock: true }
];

// find() β€” stop at the first match (don't filter then take [0])
const firstCheap = products.find(p => p.price < 700);   // { name: 'Phone', ... }

// some() / every() β€” quick boolean checks
console.log(products.some(p => p.price > 900));  // true  β€” at least one
console.log(products.every(p => p.inStock));     // true  β€” all of them

πŸ’‘ A note on performance

Each chained method loops the array once, so filter().map() is two passes. For everyday data sizes that's completely fine β€” favor readability. Only reach for a single combined reduce (or a plain loop) when profiling shows a genuinely hot path over very large arrays.

Practice & Quiz

πŸ‹οΈ Exercise 1: Celsius β†’ Fahrenheit with map

Goal: Write celsiusToFahrenheit(temps) that returns a new array of Fahrenheit values using map.

function celsiusToFahrenheit(temps) {
    // TODO: map each celsius value to (c * 9/5) + 32
}
console.log(celsiusToFahrenheit([0, 10, 20, 30, 40]));
// should log: [32, 50, 68, 86, 104]
πŸ’‘ Hint

The transform is a one-liner: each element c becomes (c * 9 / 5) + 32.

βœ… Solution
function celsiusToFahrenheit(temps) {
    return temps.map(c => (c * 9 / 5) + 32);
}
console.log(celsiusToFahrenheit([0, 10, 20, 30, 40])); // [32, 50, 68, 86, 104]

πŸ‹οΈ Exercise 2: Strip falsy values with filter

Goal: Write removeFalsy(arr) that returns a new array with every falsy value removed.

πŸ’‘ Hint

filter keeps elements whose callback returns true. The Boolean function converts any value to exactly that.

βœ… Solution
function removeFalsy(arr) {
    return arr.filter(Boolean);
}
const mixed = [0, 1, false, 2, '', 3, null, undefined, 4, NaN, 5];
console.log(removeFalsy(mixed)); // [1, 2, 3, 4, 5]

πŸ‹οΈ Exercise 3: Rebuild map using reduce

Goal: Implement mapWithReduce(arr, fn) that behaves like arr.map(fn) but is built on reduce β€” a great way to prove you understand the accumulator.

βœ… Solution
function mapWithReduce(arr, callback) {
    return arr.reduce((acc, item, index) => {
        acc.push(callback(item, index, arr));  // start with [], push each result
        return acc;
    }, []);
}
console.log(mapWithReduce([1, 2, 3, 4, 5], x => x * 2)); // [2, 4, 6, 8, 10]

πŸ‹οΈ Exercise 4: Average grade of passing students

Goal: Chain filter, map, and reduce to compute the average grade of the active students who are passing (average β‰₯ 70).

βœ… Solution
const students = [
    { name: 'Alice', grades: [85, 92, 88], status: 'active'   },
    { name: 'Bob',   grades: [75, 68, 72], status: 'active'   },
    { name: 'Carol', grades: [92, 94, 90], status: 'active'   },
    { name: 'Dave',  grades: [65, 70, 68], status: 'inactive' },
    { name: 'Eve',   grades: [95, 98, 92], status: 'active'   }
];

function passingAverage(students, passingGrade = 70) {
    const averages = students
        .filter(s => s.status === 'active')
        .map(s => s.grades.reduce((sum, g) => sum + g, 0) / s.grades.length)
        .filter(avg => avg >= passingGrade);

    if (averages.length === 0) return 0;
    return averages.reduce((sum, avg) => sum + avg, 0) / averages.length;
}

console.log(passingAverage(students).toFixed(2)); // "90.11"

🎯 Quick Quiz

Question 1: Which method always returns a new array the same length as the original?

Question 2: What is the purpose of reduce's second argument?

Question 3: You need just the first product under $700. Which is the best fit?

Best Practices & Pitfalls

βœ… Do

  • Match the method to the result shape: map for a new array, filter for a subset, reduce for one value
  • Always pass reduce an explicit initialValue
  • Keep callbacks small and pure β€” no side effects, no mutating outer state
  • Use find, some, and every for single-item and boolean questions

❌ Don't

  • Use map when you're ignoring the returned array β€” that's what forEach (or a loop) is for
  • Forget the parentheses when returning an object literal from an arrow: x => ({ ... })
  • Mutate the original array or its objects inside a callback
  • Cram every operation into one clever reduce when a readable chain is clearer

⚠️ map vs forEach

// ❌ map used only for its side effect β€” wasteful, returns an unused array
items.map(item => console.log(item));

// βœ… use forEach when you just want to DO something per element
items.forEach(item => console.log(item));

// βœ… use map when you want a NEW array back
const labels = items.map(item => item.name);

Summary

πŸŽ‰ Key Takeaways

  • map() transforms each element β†’ a new array of the same length
  • filter() keeps elements that pass a test β†’ a shorter-or-equal array
  • reduce() accumulates β†’ a single value of any type (give it an initial value)
  • All three are non-mutating and chainable into readable pipelines
  • Reach for find, some, every for single-item and yes/no questions

πŸ“š Additional Resources

πŸš€ What's Next?

You've mastered storing and transforming data in memory. Now it's time to put that data on the screen: the next lesson begins the DOM, starting with Selecting DOM Elements β€” how JavaScript reaches into the HTML page.

πŸŽ‰ Excellent!

map, filter, and reduce are the backbone of data work in JavaScript. Every framework you'll learn leans on them β€” you're now fluent in the essentials.