JavaScript Essentials

Array Iteration: forEach, map, filter, reduce

45 min Lesson 15 of 60

Introduction to Array Iteration Methods

JavaScript arrays come with a powerful set of built-in methods that let you iterate over elements, transform data, and extract meaningful results -- all without writing traditional for loops. These methods are called higher-order functions because they accept other functions as arguments. Mastering them is essential for writing clean, readable, and maintainable JavaScript code. In modern JavaScript development, array iteration methods are the backbone of data processing, whether you are filtering products in an e-commerce application, calculating totals in a shopping cart, or transforming API responses into a format your UI components can render.

Before these methods existed, developers relied on for loops and manual index tracking to iterate over arrays. While for loops still work perfectly fine, the functional iteration methods offer significant advantages: they are more expressive, they reduce the chance of off-by-one errors, and they encourage immutable data patterns that make your code easier to reason about. Each method serves a specific purpose, and understanding when to use which method is a skill that separates beginner JavaScript developers from intermediate ones.

The forEach Method

The forEach method executes a provided function once for each element in the array. It is the simplest iteration method and serves as a direct replacement for a basic for loop. Unlike the other methods we will cover, forEach does not return a new array -- it simply runs a function for each element and returns undefined.

Basic Syntax

The forEach method takes a callback function that receives up to three arguments: the current element, the current index, and the entire array being iterated.

Example: forEach Basic Syntax

const fruits = ['apple', 'banana', 'cherry', 'date'];

// Basic usage -- just the element
fruits.forEach(function(fruit) {
    console.log(fruit);
});
// Output: apple, banana, cherry, date

// With arrow function syntax
fruits.forEach(fruit => console.log(fruit));

// Using index parameter
fruits.forEach(function(fruit, index) {
    console.log(index + ': ' + fruit);
});
// Output: 0: apple, 1: banana, 2: cherry, 3: date

// Using all three parameters
fruits.forEach(function(fruit, index, array) {
    console.log(fruit + ' is item ' + (index + 1) + ' of ' + array.length);
});
// Output: apple is item 1 of 4, banana is item 2 of 4, ...

When to Use forEach

Use forEach when you need to perform a side effect for each element -- such as logging, updating the DOM, sending analytics events, or pushing values into an external data structure. It is important to understand that forEach always returns undefined, so you cannot chain it with other array methods. If you need to transform data or produce a new array, use map instead.

Example: forEach for DOM Updates

const menuItems = ['Home', 'About', 'Services', 'Contact'];
const navList = document.querySelector('#nav-list');

menuItems.forEach(function(item) {
    const li = document.createElement('li');
    li.textContent = item;
    navList.appendChild(li);
});
Note: You cannot break out of a forEach loop early. If you need to stop iteration when a condition is met, use a regular for loop, for...of, or the some or every methods instead.

The map Method

The map method creates a new array by calling a provided function on every element in the original array. The new array will always have the same length as the original array. This is the go-to method for transforming data -- taking an array of values in one shape and producing an array of values in another shape.

Basic Syntax and Transformation

Example: map Transformations

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

// Double each number
const doubled = numbers.map(function(num) {
    return num * 2;
});
console.log(doubled); // [2, 4, 6, 8, 10]
console.log(numbers); // [1, 2, 3, 4, 5] -- original unchanged

// Convert to strings
const strings = numbers.map(num => String(num));
console.log(strings); // ['1', '2', '3', '4', '5']

// Extract specific properties from objects
const users = [
    { name: 'Alice', age: 30 },
    { name: 'Bob', age: 25 },
    { name: 'Charlie', age: 35 }
];

const names = users.map(user => user.name);
console.log(names); // ['Alice', 'Bob', 'Charlie']

Transforming Objects with map

One of the most common uses of map in real-world applications is transforming arrays of objects. You might receive data from an API in one format and need to reshape it for your UI components.

Example: Reshaping API Data with map

// Data from an API
const apiResponse = [
    { id: 1, first_name: 'Jane', last_name: 'Doe', email_address: 'jane@example.com' },
    { id: 2, first_name: 'John', last_name: 'Smith', email_address: 'john@example.com' },
    { id: 3, first_name: 'Sara', last_name: 'Lee', email_address: 'sara@example.com' }
];

// Transform into the format your UI needs
const uiUsers = apiResponse.map(function(person) {
    return {
        userId: person.id,
        fullName: person.first_name + ' ' + person.last_name,
        email: person.email_address,
        initials: person.first_name[0] + person.last_name[0]
    };
});

console.log(uiUsers);
// [
//   { userId: 1, fullName: 'Jane Doe', email: 'jane@example.com', initials: 'JD' },
//   { userId: 2, fullName: 'John Smith', email: 'john@example.com', initials: 'JS' },
//   { userId: 3, fullName: 'Sara Lee', email: 'sara@example.com', initials: 'SL' }
// ]
Common Mistake: Forgetting to include a return statement inside your map callback. If you forget to return a value, the resulting array will be filled with undefined values. With arrow functions using implicit return (no curly braces), the return is automatic, but with a function body (curly braces), you must explicitly write return.

The filter Method

The filter method creates a new array containing only the elements that pass a test implemented by the provided function. The callback function must return a truthy or falsy value for each element. Elements where the callback returns a truthy value are included in the new array; elements where it returns a falsy value are excluded. The original array remains unchanged.

Basic Filtering

Example: filter Basics

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

// Get only even numbers
const evens = numbers.filter(function(num) {
    return num % 2 === 0;
});
console.log(evens); // [2, 4, 6, 8, 10]

// Get numbers greater than 5
const greaterThanFive = numbers.filter(num => num > 5);
console.log(greaterThanFive); // [6, 7, 8, 9, 10]

// Filter strings by length
const words = ['cat', 'elephant', 'dog', 'hippopotamus', 'ant'];
const longWords = words.filter(word => word.length > 4);
console.log(longWords); // ['elephant', 'hippopotamus']

Filtering Objects

In real-world applications, you will frequently filter arrays of objects based on one or more conditions. This is especially common in e-commerce product listings, search results, and data dashboards.

Example: Filtering Products

const products = [
    { name: 'Laptop', price: 999, category: 'electronics', inStock: true },
    { name: 'Shirt', price: 29, category: 'clothing', inStock: true },
    { name: 'Headphones', price: 199, category: 'electronics', inStock: false },
    { name: 'Book', price: 15, category: 'books', inStock: true },
    { name: 'Monitor', price: 449, category: 'electronics', inStock: true },
    { name: 'Jeans', price: 59, category: 'clothing', inStock: false }
];

// Get only electronics that are in stock
const availableElectronics = products.filter(function(product) {
    return product.category === 'electronics' && product.inStock;
});
console.log(availableElectronics);
// [{ name: 'Laptop', ... }, { name: 'Monitor', ... }]

// Get affordable products (under $50) that are available
const affordableAvailable = products.filter(function(product) {
    return product.price < 50 && product.inStock;
});
console.log(affordableAvailable);
// [{ name: 'Shirt', ... }, { name: 'Book', ... }]
Pro Tip: The filter method can return an empty array if no elements match the condition. Always account for this in your code -- for example, check the length of the filtered array before trying to access its elements.

The reduce Method

The reduce method is the most powerful and versatile of all array iteration methods. It executes a reducer callback function on each element of the array, passing the return value from one iteration to the next, ultimately reducing the entire array down to a single accumulated value. That value can be a number, a string, an object, another array, or any other data type.

Basic Syntax

The reduce method takes two arguments: a reducer callback function and an optional initial value. The reducer callback receives four parameters: the accumulator (the accumulated result from previous iterations), the current element, the current index, and the source array. Always provide an initial value to avoid unexpected behavior with empty arrays.

Example: reduce Basics -- Summing Numbers

const numbers = [10, 20, 30, 40, 50];

// Sum all numbers
const total = numbers.reduce(function(accumulator, currentValue) {
    return accumulator + currentValue;
}, 0);
console.log(total); // 150

// How it works step by step:
// Step 1: accumulator = 0 (initial), current = 10 => returns 10
// Step 2: accumulator = 10, current = 20 => returns 30
// Step 3: accumulator = 30, current = 30 => returns 60
// Step 4: accumulator = 60, current = 40 => returns 100
// Step 5: accumulator = 100, current = 50 => returns 150

// With arrow function
const sum = numbers.reduce((acc, num) => acc + num, 0);

Practical Uses of reduce

Beyond simple summation, reduce can build objects, flatten arrays, count occurrences, find maximum values, and perform virtually any accumulation operation.

Example: Shopping Cart Total with reduce

const cart = [
    { name: 'Widget', price: 25.99, quantity: 3 },
    { name: 'Gadget', price: 49.99, quantity: 1 },
    { name: 'Doohickey', price: 9.99, quantity: 5 }
];

// Calculate total cost
const cartTotal = cart.reduce(function(total, item) {
    return total + (item.price * item.quantity);
}, 0);
console.log(cartTotal.toFixed(2)); // '177.91'

// Count total items in cart
const totalItems = cart.reduce(function(count, item) {
    return count + item.quantity;
}, 0);
console.log(totalItems); // 9

Example: Counting Occurrences with reduce

const fruits = ['apple', 'banana', 'apple', 'cherry', 'banana', 'apple'];

const fruitCounts = fruits.reduce(function(counts, fruit) {
    counts[fruit] = (counts[fruit] || 0) + 1;
    return counts;
}, {});

console.log(fruitCounts);
// { apple: 3, banana: 2, cherry: 1 }

Example: Grouping Data with reduce

const people = [
    { name: 'Alice', department: 'Engineering' },
    { name: 'Bob', department: 'Marketing' },
    { name: 'Charlie', department: 'Engineering' },
    { name: 'Diana', department: 'Marketing' },
    { name: 'Eve', department: 'Sales' }
];

const grouped = people.reduce(function(groups, person) {
    const dept = person.department;
    if (!groups[dept]) {
        groups[dept] = [];
    }
    groups[dept].push(person.name);
    return groups;
}, {});

console.log(grouped);
// {
//   Engineering: ['Alice', 'Charlie'],
//   Marketing: ['Bob', 'Diana'],
//   Sales: ['Eve']
// }
Common Mistake: Forgetting to provide the initial value (second argument) to reduce. Without an initial value, reduce uses the first element of the array as the initial accumulator, which can cause errors with empty arrays and unexpected behavior when reducing arrays of objects. Always provide an explicit initial value.

The find and findIndex Methods

While filter returns all matching elements, sometimes you only need the first element that matches a condition. The find method returns the first element in the array that satisfies the provided testing function. If no element matches, it returns undefined. The findIndex method works similarly but returns the index of the first matching element, or -1 if no match is found.

Example: find and findIndex

const users = [
    { id: 1, name: 'Alice', role: 'admin' },
    { id: 2, name: 'Bob', role: 'user' },
    { id: 3, name: 'Charlie', role: 'moderator' },
    { id: 4, name: 'Diana', role: 'user' }
];

// Find the first admin
const admin = users.find(function(user) {
    return user.role === 'admin';
});
console.log(admin); // { id: 1, name: 'Alice', role: 'admin' }

// Find user by ID
const user = users.find(u => u.id === 3);
console.log(user.name); // 'Charlie'

// Find the index of a specific user
const bobIndex = users.findIndex(u => u.name === 'Bob');
console.log(bobIndex); // 1

// When nothing is found
const superAdmin = users.find(u => u.role === 'superadmin');
console.log(superAdmin); // undefined

const missingIndex = users.findIndex(u => u.name === 'Zara');
console.log(missingIndex); // -1

The some and every Methods

The some method tests whether at least one element in the array passes the provided test. It returns true as soon as it finds a matching element and stops iterating. The every method tests whether all elements pass the test. It returns false as soon as it finds an element that does not match. Both methods return a boolean value.

Example: some and every

const scores = [85, 92, 78, 95, 88];

// Did anyone score above 90?
const hasHighScore = scores.some(score => score > 90);
console.log(hasHighScore); // true

// Did everyone pass (score above 70)?
const allPassed = scores.every(score => score > 70);
console.log(allPassed); // true

// Did everyone score above 80?
const allAbove80 = scores.every(score => score > 80);
console.log(allAbove80); // false (78 fails)

// Practical example: form validation
const formFields = [
    { name: 'email', value: 'user@example.com', valid: true },
    { name: 'password', value: 'secret123', valid: true },
    { name: 'age', value: '', valid: false }
];

const isFormValid = formFields.every(field => field.valid);
console.log(isFormValid); // false

const hasAnyInput = formFields.some(field => field.value.length > 0);
console.log(hasAnyInput); // true
Note: Both some and every use short-circuit evaluation. The some method stops as soon as it finds a true result, and every stops as soon as it finds a false result. This makes them more efficient than iterating through the entire array when an early answer is possible.

Chaining Array Methods

One of the greatest strengths of these array methods is that they can be chained together. Since map and filter return new arrays, you can call another array method immediately on the result. This lets you build complex data processing pipelines that are both powerful and readable.

Example: Chaining filter, map, and reduce

const orders = [
    { product: 'Laptop', amount: 999, status: 'completed' },
    { product: 'Mouse', amount: 29, status: 'completed' },
    { product: 'Keyboard', amount: 79, status: 'cancelled' },
    { product: 'Monitor', amount: 449, status: 'completed' },
    { product: 'Webcam', amount: 89, status: 'pending' },
    { product: 'Desk', amount: 299, status: 'completed' }
];

// Calculate total revenue from completed orders
const totalRevenue = orders
    .filter(order => order.status === 'completed')
    .reduce((sum, order) => sum + order.amount, 0);
console.log(totalRevenue); // 1776

// Get names of completed orders over $100, formatted
const bigCompletedOrders = orders
    .filter(order => order.status === 'completed')
    .filter(order => order.amount > 100)
    .map(order => order.product + ' ($' + order.amount + ')');
console.log(bigCompletedOrders);
// ['Laptop ($999)', 'Monitor ($449)', 'Desk ($299)']

Example: Complex Data Pipeline

const employees = [
    { name: 'Alice', department: 'Engineering', salary: 95000, active: true },
    { name: 'Bob', department: 'Marketing', salary: 72000, active: true },
    { name: 'Charlie', department: 'Engineering', salary: 88000, active: false },
    { name: 'Diana', department: 'Engineering', salary: 102000, active: true },
    { name: 'Eve', department: 'Marketing', salary: 68000, active: true },
    { name: 'Frank', department: 'Sales', salary: 78000, active: true }
];

// Get average salary of active engineering employees
const activeEngineers = employees
    .filter(emp => emp.active && emp.department === 'Engineering');

const avgSalary = activeEngineers
    .reduce((sum, emp) => sum + emp.salary, 0) / activeEngineers.length;

console.log(avgSalary); // 98500

// Create a department summary
const departmentSummary = employees
    .filter(emp => emp.active)
    .reduce(function(summary, emp) {
        if (!summary[emp.department]) {
            summary[emp.department] = { count: 0, totalSalary: 0 };
        }
        summary[emp.department].count += 1;
        summary[emp.department].totalSalary += emp.salary;
        return summary;
    }, {});

console.log(departmentSummary);
// {
//   Engineering: { count: 2, totalSalary: 197000 },
//   Marketing: { count: 2, totalSalary: 140000 },
//   Sales: { count: 1, totalSalary: 78000 }
// }

Performance Considerations

While array iteration methods are elegant and readable, it is important to understand their performance characteristics, especially when working with large datasets.

  • Chaining creates intermediate arrays -- When you chain filter().map(), the filter method creates a temporary array that map then iterates over. For very large arrays (hundreds of thousands of elements), this can consume extra memory. In such cases, consider using a single reduce to perform both operations in one pass.
  • forEach vs for loop -- A traditional for loop is slightly faster than forEach because it avoids the overhead of function calls. However, the difference is negligible for most applications. Prioritize readability unless you are dealing with performance-critical code processing millions of elements.
  • Early termination -- Methods like find, findIndex, some, and every stop iterating as soon as a result is determined. Use them instead of filter when you only need the first match or a boolean answer.
  • Avoid mutations -- Never modify the original array inside a map, filter, or forEach callback. This leads to unpredictable bugs. Always treat the source array as read-only and build new data structures instead.

Example: Optimizing with a Single reduce Instead of Chaining

const data = [
    { value: 10, active: true },
    { value: 20, active: false },
    { value: 30, active: true },
    { value: 40, active: true },
    { value: 50, active: false }
];

// Less efficient: two passes through the data
const result1 = data
    .filter(item => item.active)
    .map(item => item.value * 2);

// More efficient: single pass
const result2 = data.reduce(function(acc, item) {
    if (item.active) {
        acc.push(item.value * 2);
    }
    return acc;
}, []);

// Both produce [20, 60, 80]

Real-World Data Processing Examples

Let us put everything together with comprehensive examples that mirror real-world scenarios you will encounter in professional JavaScript development.

Example: E-Commerce Product Filtering and Display

const products = [
    { id: 1, name: 'Wireless Mouse', price: 29.99, rating: 4.5, category: 'accessories', inStock: true },
    { id: 2, name: 'Mechanical Keyboard', price: 89.99, rating: 4.8, category: 'accessories', inStock: true },
    { id: 3, name: 'USB Hub', price: 19.99, rating: 3.9, category: 'accessories', inStock: false },
    { id: 4, name: 'Laptop Stand', price: 45.99, rating: 4.2, category: 'furniture', inStock: true },
    { id: 5, name: 'Monitor Light', price: 59.99, rating: 4.7, category: 'lighting', inStock: true },
    { id: 6, name: 'Desk Pad', price: 24.99, rating: 4.1, category: 'accessories', inStock: true }
];

// 1. Get in-stock accessories sorted by rating
const topAccessories = products
    .filter(p => p.category === 'accessories' && p.inStock)
    .sort((a, b) => b.rating - a.rating)
    .map(p => p.name + ' - $' + p.price + ' (Rating: ' + p.rating + ')');

console.log(topAccessories);
// ['Mechanical Keyboard - $89.99 (Rating: 4.8)',
//  'Wireless Mouse - $29.99 (Rating: 4.5)',
//  'Desk Pad - $24.99 (Rating: 4.1)']

// 2. Calculate average price of in-stock products
const inStockProducts = products.filter(p => p.inStock);
const avgPrice = inStockProducts
    .reduce((sum, p) => sum + p.price, 0) / inStockProducts.length;
console.log('Average price: $' + avgPrice.toFixed(2)); // Average price: $50.19

// 3. Check if any product needs restocking
const needsRestock = products.some(p => !p.inStock);
console.log('Needs restock:', needsRestock); // true

// 4. Group products by category with counts
const categoryStats = products.reduce(function(stats, product) {
    const cat = product.category;
    if (!stats[cat]) {
        stats[cat] = { total: 0, inStock: 0, avgRating: 0, ratings: [] };
    }
    stats[cat].total += 1;
    if (product.inStock) stats[cat].inStock += 1;
    stats[cat].ratings.push(product.rating);
    return stats;
}, {});

// Calculate average ratings per category
Object.keys(categoryStats).forEach(function(cat) {
    const ratings = categoryStats[cat].ratings;
    categoryStats[cat].avgRating = ratings.reduce((s, r) => s + r, 0) / ratings.length;
    delete categoryStats[cat].ratings;
});

console.log(categoryStats);

Example: Transforming API Response Data

// Simulated API response
const apiData = {
    status: 'success',
    results: [
        { user_id: 101, first_name: 'Ahmad', last_name: 'Hassan',
          email: 'ahmad@example.com', created_at: '2024-01-15', orders_count: 12 },
        { user_id: 102, first_name: 'Sara', last_name: 'Ali',
          email: 'sara@example.com', created_at: '2024-03-22', orders_count: 0 },
        { user_id: 103, first_name: 'Omar', last_name: 'Khan',
          email: 'omar@example.com', created_at: '2023-11-08', orders_count: 45 },
        { user_id: 104, first_name: 'Layla', last_name: 'Mahmoud',
          email: 'layla@example.com', created_at: '2024-06-01', orders_count: 7 }
    ]
};

// Transform into UI-friendly format for active customers only
const activeCustomers = apiData.results
    .filter(user => user.orders_count > 0)
    .map(function(user) {
        return {
            id: user.user_id,
            displayName: user.first_name + ' ' + user.last_name,
            email: user.email,
            memberSince: new Date(user.created_at).toLocaleDateString(),
            isVIP: user.orders_count > 10,
            tier: user.orders_count > 20 ? 'gold' : user.orders_count > 10 ? 'silver' : 'bronze'
        };
    });

console.log(activeCustomers);

// Summary statistics
const stats = apiData.results.reduce(function(acc, user) {
    acc.totalOrders += user.orders_count;
    acc.activeUsers += user.orders_count > 0 ? 1 : 0;
    return acc;
}, { totalOrders: 0, activeUsers: 0 });

console.log('Total orders: ' + stats.totalOrders); // 64
console.log('Active users: ' + stats.activeUsers); // 3
Pro Tip: When chaining multiple methods, break the chain across multiple lines with each method on its own line, indented under the array variable. This makes the data pipeline easy to read, debug, and modify. You can also add comments before each step to explain its purpose.

Practice Exercise

Create an array of at least 8 student objects, each with properties for name, grade (a number from 0-100), subject (one of 'math', 'science', 'english'), and enrolled (boolean). Then complete these tasks using array iteration methods:

  1. Use filter to get all enrolled students with a grade above 75.
  2. Use map to create an array of strings in the format "Name: grade% (subject)".
  3. Use reduce to calculate the average grade across all enrolled students.
  4. Use find to locate the first student in the math subject with the highest grade.
  5. Use some to check if any student is failing (grade below 50).
  6. Use every to check if all enrolled students have a grade above 30.
  7. Chain filter and reduce to find the total grade points for enrolled science students.
  8. Use reduce to group students by subject and count how many are enrolled in each.