Episode 1 — Fundamentals / 1.22 — Array Methods

1.22.i — Functional Thinking with Array Methods

In one sentence: Combining non-mutating array methods like filter, map, and reduce into chains lets you write declarative, readable data transformations that tell the reader what happens, not how each step is implemented.

Navigation: ← 1.22.h — flat() and flatMap() · 1.22.j — Practice Problems →


1. Imperative vs declarative style

Imperative: tells the computer HOW (step by step)

const users = [
  { name: "Alice", age: 28, active: true },
  { name: "Bob", age: 17, active: true },
  { name: "Carol", age: 34, active: false },
  { name: "Dave", age: 22, active: true },
  { name: "Eve", age: 19, active: true },
];

// Get names of active adults, sorted alphabetically
const result = [];
for (let i = 0; i < users.length; i++) {
  if (users[i].active && users[i].age >= 18) {
    result.push(users[i].name);
  }
}
result.sort();
console.log(result); // ["Alice", "Dave", "Eve"]

Declarative: tells the computer WHAT you want

const result = users
  .filter(u => u.active && u.age >= 18)
  .map(u => u.name)
  .sort();

console.log(result); // ["Alice", "Dave", "Eve"]

The declarative version reads like a sentence: "Filter active adults, map to names, sort."


2. Method chaining — composing transformations

Method chaining works because each method returns a value (usually an array) that has the next method available on it.

const orders = [
  { product: "Laptop", price: 999, qty: 1, category: "electronics" },
  { product: "Shirt", price: 29, qty: 3, category: "clothing" },
  { product: "Phone", price: 699, qty: 1, category: "electronics" },
  { product: "Book", price: 15, qty: 5, category: "books" },
  { product: "Pants", price: 49, qty: 2, category: "clothing" },
  { product: "Headphones", price: 149, qty: 1, category: "electronics" },
];

const electronicsSummary = orders
  .filter(o => o.category === "electronics")        // Step 1: select electronics
  .map(o => ({ ...o, total: o.price * o.qty }))     // Step 2: add total field
  .sort((a, b) => b.total - a.total)                // Step 3: sort by total desc
  .map(o => `${o.product}: $${o.total}`);           // Step 4: format as strings

console.log(electronicsSummary);
// ["Laptop: $999", "Phone: $699", "Headphones: $149"]

Each line is a transformation step. Reading top to bottom tells you exactly what the pipeline does.


3. The immutability principle

Prefer non-mutating methods whenever possible:

Non-mutating (safe)Mutating (caution)
map()sort()
filter()splice()
reduce()reverse()
slice()push() / pop()
flat() / flatMap()shift() / unshift()
concat()fill()
toSorted() (ES2023)
toReversed() (ES2023)
toSpliced() (ES2023)

Why immutability matters

  1. Predictability — Functions that do not mutate inputs are easier to reason about.
  2. Debugging — You can inspect the original data at any point.
  3. React/Redux — State updates must be immutable for change detection to work.
  4. Concurrency — No race conditions from shared mutable state.
// DANGEROUS: mutates the input
function getSortedNames(users) {
  return users.sort((a, b) => a.name.localeCompare(b.name)).map(u => u.name);
  // Caller's array is now sorted! Side effect.
}

// SAFE: does not mutate
function getSortedNames(users) {
  return [...users]
    .sort((a, b) => a.name.localeCompare(b.name))
    .map(u => u.name);
}

4. The pipeline pattern: filter, map, reduce

The most common functional pipeline follows this shape:

Raw data  →  filter (select)  →  map (transform)  →  reduce (aggregate)

Example: e-commerce report

const transactions = [
  { id: 1, amount: 250, type: "sale", region: "US" },
  { id: 2, amount: 150, type: "refund", region: "EU" },
  { id: 3, amount: 500, type: "sale", region: "US" },
  { id: 4, amount: 75, type: "sale", region: "EU" },
  { id: 5, amount: 300, type: "sale", region: "US" },
  { id: 6, amount: 100, type: "refund", region: "US" },
];

// Total US sales revenue
const usSalesTotal = transactions
  .filter(t => t.type === "sale" && t.region === "US")   // select: US sales only
  .map(t => t.amount)                                     // transform: extract amounts
  .reduce((sum, amount) => sum + amount, 0);               // aggregate: sum

console.log(usSalesTotal); // 1050

Example: student grade report

const students = [
  { name: "Alice", scores: [85, 92, 78] },
  { name: "Bob", scores: [45, 52, 38] },
  { name: "Carol", scores: [91, 96, 88] },
  { name: "Dave", scores: [72, 68, 75] },
];

const honorRoll = students
  .map(s => ({
    name: s.name,
    avg: s.scores.reduce((sum, sc) => sum + sc, 0) / s.scores.length,
  }))
  .filter(s => s.avg >= 80)
  .sort((a, b) => b.avg - a.avg)
  .map(s => `${s.name} (${s.avg.toFixed(1)})`);

console.log(honorRoll);
// ["Carol (91.7)", "Alice (85.0)"]

5. When NOT to use functional methods

Performance-critical tight loops

For extremely large datasets (millions of elements) or hot code paths:

// Functional: creates 3 intermediate arrays
const result = hugeArray
  .filter(x => x > 0)
  .map(x => x * 2)
  .reduce((sum, x) => sum + x, 0);

// Imperative: single pass, no intermediate arrays
let result2 = 0;
for (let i = 0; i < hugeArray.length; i++) {
  if (hugeArray[i] > 0) {
    result2 += hugeArray[i] * 2;
  }
}

The imperative version does one pass and allocates zero intermediate arrays. For most applications, the functional version is fast enough. Only optimize if profiling shows it is a bottleneck.

When you need early exit

// filter visits ALL elements, even after finding the answer
const hasAdmin = users.filter(u => u.role === "admin").length > 0;

// some short-circuits at first match — more efficient
const hasAdmin2 = users.some(u => u.role === "admin");

// find also short-circuits
const admin = users.find(u => u.role === "admin");

When for...of is clearer

Sometimes a plain loop with break, continue, or await is simply more readable:

// Awkward functional approach with side effects
let found = null;
items.forEach(item => {
  if (!found && item.matches(query)) found = item;
});

// Cleaner: just use for...of
for (const item of items) {
  if (item.matches(query)) {
    found = item;
    break;
  }
}

// Best: use find
const found2 = items.find(item => item.matches(query));

6. Readability balance — don't over-chain

Too much chaining (unreadable)

// What does this even do?
const result = data
  .filter(d => d.active)
  .map(d => ({ ...d, score: d.points / d.games }))
  .filter(d => d.score > 10)
  .sort((a, b) => b.score - a.score)
  .slice(0, 5)
  .map(d => d.name)
  .reduce((acc, name, i) => acc + `${i + 1}. ${name}\n`, "");

Better: break into named steps

// Name your intermediate results for clarity
const activePlayers = data.filter(d => d.active);

const withScores = activePlayers.map(d => ({
  ...d,
  score: d.points / d.games,
}));

const topPerformers = withScores
  .filter(d => d.score > 10)
  .sort((a, b) => b.score - a.score)
  .slice(0, 5);

const leaderboard = topPerformers
  .map((d, i) => `${i + 1}. ${d.name}`)
  .join("\n");

Guidelines:

  • 3-4 chained methods is usually comfortable to read.
  • If the chain exceeds 5-6 steps, extract intermediate variables with descriptive names.
  • Each step should do one thing (filter OR map OR sort, not a complex reduce that does everything).

7. Real-world pipeline: API data processing

// Raw API response
const apiResponse = {
  users: [
    { id: 1, name: "Alice Smith", email: "alice@co.com", role: "admin", lastLogin: "2024-03-15" },
    { id: 2, name: "Bob Jones", email: "bob@co.com", role: "user", lastLogin: "2024-01-02" },
    { id: 3, name: "Carol White", email: "carol@co.com", role: "user", lastLogin: "2024-03-14" },
    { id: 4, name: "Dave Brown", email: null, role: "user", lastLogin: "2023-06-10" },
    { id: 5, name: "Eve Davis", email: "eve@co.com", role: "admin", lastLogin: "2024-03-16" },
  ],
};

// Pipeline: get active admins with valid emails, formatted for a dropdown
const adminOptions = apiResponse.users
  .filter(u => u.role === "admin")                         // 1. Only admins
  .filter(u => u.email)                                    // 2. Must have email
  .filter(u => {                                           // 3. Logged in within 30 days
    const daysSince = (Date.now() - new Date(u.lastLogin)) / (1000 * 60 * 60 * 24);
    return daysSince <= 30;
  })
  .map(u => ({                                             // 4. Shape for dropdown
    value: u.id,
    label: `${u.name} (${u.email})`,
  }))
  .sort((a, b) => a.label.localeCompare(b.label));         // 5. Sort alphabetically

console.log(adminOptions);
// [
//   { value: 1, label: "Alice Smith (alice@co.com)" },
//   { value: 5, label: "Eve Davis (eve@co.com)" },
// ]

8. Comparison: imperative loop vs functional chain

Problem: From an array of orders, calculate the total revenue per category for in-stock items.

Imperative

const totals = {};
for (let i = 0; i < orders.length; i++) {
  const order = orders[i];
  if (!order.inStock) continue;
  const category = order.category;
  if (!totals[category]) {
    totals[category] = 0;
  }
  totals[category] += order.price * order.qty;
}

Functional

const totals = orders
  .filter(o => o.inStock)
  .reduce((acc, o) => {
    acc[o.category] = (acc[o.category] || 0) + o.price * o.qty;
    return acc;
  }, {});

Both produce the same result. The functional version separates filtering from aggregation, making each concern independently testable and readable.


9. Composing reusable utilities

Extract common operations into named functions:

// Reusable predicates
const isActive = user => user.active;
const isAdult = user => user.age >= 18;
const isAdmin = user => user.role === "admin";

// Reusable transforms
const toDisplayName = user => `${user.firstName} ${user.lastName}`;
const toOption = user => ({ value: user.id, label: toDisplayName(user) });

// Compose them
const adminOptions = users
  .filter(isActive)
  .filter(isAdult)
  .filter(isAdmin)
  .map(toOption)
  .sort((a, b) => a.label.localeCompare(b.label));

This is highly readable and each function can be unit-tested independently.


10. The reduce trap

reduce is powerful but often overused. A common anti-pattern:

// Over-engineered: doing everything in one reduce
const result = data.reduce((acc, item) => {
  if (item.active) {
    const transformed = { ...item, total: item.price * item.qty };
    if (transformed.total > 100) {
      acc.items.push(transformed);
      acc.total += transformed.total;
      acc.count++;
    }
  }
  return acc;
}, { items: [], total: 0, count: 0 });
// Clearer: separate concerns
const activeItems = data
  .filter(item => item.active)
  .map(item => ({ ...item, total: item.price * item.qty }))
  .filter(item => item.total > 100);

const result = {
  items: activeItems,
  total: activeItems.reduce((sum, item) => sum + item.total, 0),
  count: activeItems.length,
};

The second version is more readable, more debuggable, and each step is independently testable.


11. Summary table: which method for which job

TaskMethodReturns
Transform each elementmap()Array (same length)
Select elements by conditionfilter()Array (subset)
Accumulate into single valuereduce()Any type
Side effect per elementforEach()undefined
Check if any element passessome()Boolean
Check if all elements passevery()Boolean
Find first matching elementfind()Element or undefined
Find first matching indexfindIndex()Number (-1 if none)
Extract portionslice()Array (new)
Flatten nestingflat() / flatMap()Array (new)
SorttoSorted() / sort()Array

Key takeaways

  1. Declarative chains (filtermapreduce) tell the reader what is happening.
  2. Prefer non-mutating methods — copy before sort, use slice instead of splice.
  3. The classic pipeline is: filter (select) → map (transform) → reduce (aggregate).
  4. Do not over-chain — break into named intermediates when chains exceed 4-5 steps.
  5. Use imperative loops when you need early exit, await, or maximum performance.
  6. Extract named predicates and transforms for reusable, testable code.

Explain-It Challenge

Explain without notes:

  1. Why should you generally filter before mapping in a chain?
  2. Name three mutating array methods and their non-mutating alternatives.
  3. When is a for loop better than a functional chain?

Navigation: ← 1.22.h — flat() and flatMap() · 1.22.j — Practice Problems →