DevToolBox免费
博客

JavaScript map()、filter()、reduce() 示例

11 分钟阅读作者 DevToolBox

JavaScript 的 map()filter()reduce() 是函数式数组处理的三大支柱。它们让你在不修改原数组的情况下进行数据转换、筛选和聚合。结合方法链式调用,它们能构建出比命令式循环更易读、易测试、易维护的数据处理管道。本指南深入讲解每个方法,配有可运行的代码示例,展示输入和输出结果。

使用我们的 JSON 格式化工具检查和格式化你的 JSON 数据 →

使用我们的 JS/HTML 格式化工具美化你的代码 →

1. 概述 — JavaScript 中的函数式编程

JavaScript 中的函数式编程围绕纯函数不可变性声明式数据转换展开。三大数组方法 — map()filter()reduce() — 体现了这些原则:它们从不修改源数组,总是返回新值,并且可以链式调用以从简单的构建块组合出复杂操作。

为什么要用它们代替 for 循环?因为它们将"做什么"和"怎么做"分离。当你看到 .filter(),你立刻知道意图是筛选元素。当你看到 .map(),你知道意图是转换每个元素。这使代码自文档化,减少了越界错误或意外修改导致的 bug。

// Imperative (for loop) vs Declarative (map/filter/reduce)

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

// Imperative: get sum of squares of even numbers
let sum = 0;
for (let i = 0; i < numbers.length; i++) {
  if (numbers[i] % 2 === 0) {
    sum += numbers[i] ** 2;
  }
}
console.log(sum); // 220

// Declarative: same result, but reads like a sentence
const result = numbers
  .filter(n => n % 2 === 0)       // [2, 4, 6, 8, 10]
  .map(n => n ** 2)                // [4, 16, 36, 64, 100]
  .reduce((acc, n) => acc + n, 0); // 220

console.log(result); // 220

// Key principle: the original array is NEVER modified
console.log(numbers); // [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] — unchanged

2. map() 基础 — 语法和回调参数

Array.prototype.map() 通过对原数组的每个元素调用回调函数来创建一个新数组。它总是返回一个相同长度的数组。回调接收三个参数:当前索引和整个数组

// Syntax:
// array.map(callback(currentValue, index, array), thisArg?)

// Basic: double every number
const nums = [1, 2, 3, 4, 5];
const doubled = nums.map(n => n * 2);
console.log(doubled);
// Output: [2, 4, 6, 8, 10]

// Using all three callback parameters
const indexed = ['a', 'b', 'c'].map((value, index, array) => {
  return `${index}/${array.length}: ${value}`;
});
console.log(indexed);
// Output: ["0/3: a", "1/3: b", "2/3: c"]

// map() ALWAYS returns a new array of the same length
const original = [10, 20, 30];
const mapped = original.map(x => x);
console.log(mapped === original); // false (different reference)
console.log(mapped.length === original.length); // true

// Common gotcha: map() with parseInt
console.log(['1', '2', '3'].map(Number));     // [1, 2, 3] ✅
console.log(['1', '2', '3'].map(parseInt));    // [1, NaN, NaN] ❌
// Because parseInt receives (value, index) → parseInt('2', 1) is NaN
// Fix: wrap in arrow function
console.log(['1', '2', '3'].map(s => parseInt(s, 10))); // [1, 2, 3] ✅

3. map() 实战示例

以下是 map() 的生产级模式:转换对象、提取字段、格式化数据,以及使用 TypeScript 泛型确保类型安全。

// 3a. Transform objects — add computed fields
const users = [
  { firstName: 'Alice', lastName: 'Smith', age: 30 },
  { firstName: 'Bob', lastName: 'Jones', age: 25 },
  { firstName: 'Charlie', lastName: 'Brown', age: 35 },
];

const enriched = users.map(user => ({
  ...user,
  fullName: `${user.firstName} ${user.lastName}`,
  isAdult: user.age >= 18,
}));

console.log(enriched);
// Output:
// [
//   { firstName: 'Alice', lastName: 'Smith', age: 30, fullName: 'Alice Smith', isAdult: true },
//   { firstName: 'Bob', lastName: 'Jones', age: 25, fullName: 'Bob Jones', isAdult: true },
//   { firstName: 'Charlie', lastName: 'Brown', age: 35, fullName: 'Charlie Brown', isAdult: true },
// ]
// 3b. Extract specific fields (projection)
const products = [
  { id: 1, name: 'Laptop', price: 999, stock: 50 },
  { id: 2, name: 'Phone', price: 699, stock: 120 },
  { id: 3, name: 'Tablet', price: 499, stock: 80 },
];

const names = products.map(p => p.name);
console.log(names);
// Output: ["Laptop", "Phone", "Tablet"]

// Extract id-name pairs for a dropdown
const options = products.map(({ id, name }) => ({ value: id, label: name }));
console.log(options);
// Output: [{ value: 1, label: 'Laptop' }, { value: 2, label: 'Phone' }, { value: 3, label: 'Tablet' }]
// 3c. Format data for display
const prices = [1299.5, 49.99, 0.5, 1000];

const formatted = prices.map(p =>
  new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(p)
);
console.log(formatted);
// Output: ["$1,299.50", "$49.99", "$0.50", "$1,000.00"]

// Format dates
const timestamps = [1700000000000, 1710000000000, 1720000000000];
const dates = timestamps.map(ts =>
  new Date(ts).toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' })
);
console.log(dates);
// Output: ["Nov 14, 2023", "Mar 9, 2024", "Jul 3, 2024"]
// 3d. TypeScript typing with map()
interface User {
  id: number;
  name: string;
  email: string;
}

interface UserDTO {
  id: number;
  displayName: string;
}

const usersData: User[] = [
  { id: 1, name: 'Alice', email: 'alice@example.com' },
  { id: 2, name: 'Bob', email: 'bob@example.com' },
];

// TypeScript infers the return type as UserDTO[]
const dtos: UserDTO[] = usersData.map((user): UserDTO => ({
  id: user.id,
  displayName: user.name.toUpperCase(),
}));

console.log(dtos);
// Output: [{ id: 1, displayName: 'ALICE' }, { id: 2, displayName: 'BOB' }]

4. filter() 基础 — 真值/假值与不可变性

Array.prototype.filter() 创建一个新数组,仅包含回调返回真值的元素。它不会修改原数组,返回的数组可能更短(甚至为空)。

// Syntax:
// array.filter(callback(element, index, array), thisArg?)

// Basic: keep only even numbers
const nums = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const evens = nums.filter(n => n % 2 === 0);
console.log(evens);
// Output: [2, 4, 6, 8, 10]

// The callback must return a truthy or falsy value
// These are FALSY in JavaScript: false, 0, '', null, undefined, NaN
const mixed = [0, 1, '', 'hello', null, undefined, false, true, NaN, 42];
const truthy = mixed.filter(Boolean);
console.log(truthy);
// Output: [1, "hello", true, 42]

// filter() does NOT mutate the original
const original = [10, 20, 30, 40, 50];
const filtered = original.filter(n => n > 25);
console.log(filtered);  // [30, 40, 50]
console.log(original);  // [10, 20, 30, 40, 50] — unchanged

// filter() can return an empty array
const empty = [1, 2, 3].filter(n => n > 100);
console.log(empty); // []

5. filter() 实战示例

常见模式:移除 null/undefined、搜索数组、按属性过滤对象、提取唯一值。

// 5a. Remove nulls and undefined (type-safe)
const data = ['Alice', null, 'Bob', undefined, 'Charlie', null];

const clean = data.filter((item): item is string => item != null);
console.log(clean);
// Output: ["Alice", "Bob", "Charlie"]

// Without type guard (still works at runtime)
const clean2 = data.filter(Boolean);
console.log(clean2);
// Output: ["Alice", "Bob", "Charlie"]
// 5b. Search / text matching
const fruits = ['Apple', 'Banana', 'Avocado', 'Blueberry', 'Apricot', 'Cherry'];

const startsWithA = fruits.filter(f => f.startsWith('A'));
console.log(startsWithA);
// Output: ["Apple", "Avocado", "Apricot"]

// Case-insensitive search
function search(items: string[], query: string): string[] {
  const q = query.toLowerCase();
  return items.filter(item => item.toLowerCase().includes(q));
}

console.log(search(fruits, 'berry'));
// Output: ["Blueberry"]

console.log(search(fruits, 'an'));
// Output: ["Banana"]
// 5c. Filter objects by property
const products = [
  { name: 'Laptop', price: 999, inStock: true },
  { name: 'Phone', price: 699, inStock: false },
  { name: 'Tablet', price: 499, inStock: true },
  { name: 'Watch', price: 299, inStock: true },
  { name: 'Headphones', price: 199, inStock: false },
];

// In stock and under $500
const affordable = products.filter(p => p.inStock && p.price < 500);
console.log(affordable);
// Output: [{ name: 'Tablet', price: 499, inStock: true }, { name: 'Watch', price: 299, inStock: true }]

// Multiple conditions with a predicate builder
function createFilter(minPrice: number, maxPrice: number, mustBeInStock: boolean) {
  return (p: typeof products[0]) =>
    p.price >= minPrice &&
    p.price <= maxPrice &&
    (!mustBeInStock || p.inStock);
}

const filtered = products.filter(createFilter(200, 700, true));
console.log(filtered.map(p => p.name));
// Output: ["Tablet", "Watch"]
// 5d. Extract unique values
const tags = ['js', 'ts', 'react', 'js', 'node', 'ts', 'react', 'vue'];

// Method 1: filter with indexOf
const unique1 = tags.filter((tag, index) => tags.indexOf(tag) === index);
console.log(unique1);
// Output: ["js", "ts", "react", "node", "vue"]

// Method 2: Set (preferred for performance)
const unique2 = [...new Set(tags)];
console.log(unique2);
// Output: ["js", "ts", "react", "node", "vue"]

// Unique objects by property
const users = [
  { id: 1, name: 'Alice' },
  { id: 2, name: 'Bob' },
  { id: 1, name: 'Alice (duplicate)' },
  { id: 3, name: 'Charlie' },
];

const seen = new Set();
const uniqueUsers = users.filter(u => {
  if (seen.has(u.id)) return false;
  seen.add(u.id);
  return true;
});
console.log(uniqueUsers);
// Output: [{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }, { id: 3, name: 'Charlie' }]

6. reduce() 基础 — 累加器、当前值与初始值

Array.prototype.reduce() 对每个元素执行回调,将上一次调用的返回值作为累加器传递。reduce() 的第二个参数是初始值 — 务必提供它以避免空数组上的异常行为。

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

// Basic: sum of numbers
const nums = [10, 20, 30, 40, 50];
const sum = nums.reduce((acc, curr) => acc + curr, 0);
console.log(sum);
// Output: 150

// Step-by-step trace:
// Step 0: acc = 0 (initial),  curr = 10 → return 10
// Step 1: acc = 10,           curr = 20 → return 30
// Step 2: acc = 30,           curr = 30 → return 60
// Step 3: acc = 60,           curr = 40 → return 100
// Step 4: acc = 100,          curr = 50 → return 150

// ⚠️ Without initial value — risky!
const sum2 = [10, 20, 30].reduce((acc, curr) => acc + curr);
console.log(sum2); // 60 — works, but...

// Empty array WITHOUT initial value → TypeError!
try {
  [].reduce((acc, curr) => acc + curr);
} catch (e) {
  console.log(e.message);
  // "Reduce of empty array with no initial value"
}

// Empty array WITH initial value → safe
const sum3 = [].reduce((acc, curr) => acc + curr, 0);
console.log(sum3); // 0

7. reduce() 模式 — 求和、分组、展平、管道

reduce() 是最通用的数组方法。它几乎可以实现任何数组操作:求和、平均值、分组(groupBy)、展平(flatten)、计数和函数组合管道。

// 7a. Sum and Average
const scores = [85, 92, 78, 96, 88];

const total = scores.reduce((acc, s) => acc + s, 0);
const average = total / scores.length;

console.log(`Total: ${total}, Average: ${average}`);
// Output: "Total: 439, Average: 87.8"

// One-liner average
const avg = scores.reduce((a, b, i, arr) => a + b / arr.length, 0);
console.log(avg); // 87.8
// 7b. groupBy — group objects by a key
const people = [
  { name: 'Alice', department: 'Engineering' },
  { name: 'Bob', department: 'Marketing' },
  { name: 'Charlie', department: 'Engineering' },
  { name: 'Diana', department: 'Marketing' },
  { name: 'Eve', department: 'Design' },
];

const grouped = people.reduce((acc, person) => {
  const key = person.department;
  if (!acc[key]) acc[key] = [];
  acc[key].push(person.name);
  return acc;
}, {} as Record<string, string[]>);

console.log(grouped);
// Output:
// {
//   Engineering: ['Alice', 'Charlie'],
//   Marketing: ['Bob', 'Diana'],
//   Design: ['Eve']
// }

// ES2024+ alternative: Object.groupBy()
// const grouped2 = Object.groupBy(people, p => p.department);
// 7c. Flatten nested arrays
const nested = [[1, 2], [3, 4], [5, [6, 7]]];

// Flatten one level
const flat1 = nested.reduce((acc, curr) => acc.concat(curr), []);
console.log(flat1);
// Output: [1, 2, 3, 4, 5, [6, 7]]

// Modern alternative: Array.flat()
const flat2 = nested.flat(1);
console.log(flat2);
// Output: [1, 2, 3, 4, 5, [6, 7]]

// Deep flatten with reduce (recursive)
function deepFlatten(arr) {
  return arr.reduce((acc, val) =>
    Array.isArray(val) ? acc.concat(deepFlatten(val)) : acc.concat(val),
    []
  );
}

console.log(deepFlatten([[1, [2, [3, [4]]]], [5]]));
// Output: [1, 2, 3, 4, 5]
// 7d. Count occurrences
const fruits = ['apple', 'banana', 'apple', 'orange', 'banana', 'apple'];

const counts = fruits.reduce((acc, fruit) => {
  acc[fruit] = (acc[fruit] || 0) + 1;
  return acc;
}, {} as Record<string, number>);

console.log(counts);
// Output: { apple: 3, banana: 2, orange: 1 }

// Find the most common element
const mostCommon = Object.entries(counts)
  .reduce((max, [key, val]) => val > max[1] ? [key, val] : max, ['', 0]);

console.log(mostCommon);
// Output: ["apple", 3]
// 7e. Function composition pipeline
const pipeline = [
  (s: string) => s.trim(),
  (s: string) => s.toLowerCase(),
  (s: string) => s.replace(/\s+/g, '-'),
  (s: string) => s.replace(/[^a-z0-9-]/g, ''),
];

const slugify = (input: string) =>
  pipeline.reduce((acc, fn) => fn(acc), input);

console.log(slugify('  Hello World! @2024  '));
// Output: "hello-world-2024"

console.log(slugify('  JavaScript Map, Filter & Reduce  '));
// Output: "javascript-map-filter--reduce"

// Generic pipe function
function pipe<T>(...fns: ((arg: T) => T)[]): (arg: T) => T {
  return (input: T) => fns.reduce((acc, fn) => fn(acc), input);
}

const process = pipe(
  (n: number) => n * 2,
  (n: number) => n + 10,
  (n: number) => n / 3,
);

console.log(process(5));  // (5 * 2 + 10) / 3 = 6.666...
console.log(process(10)); // (10 * 2 + 10) / 3 = 10

8. 链式调用 map + filter + reduce — 数据处理管道

函数式数组方法的真正威力在于链式调用。链中的每个方法接收上一个方法的输出,形成清晰、可读的数据处理管道。

// Real-world example: process e-commerce order data
const orders = [
  { id: 1, customer: 'Alice', total: 250, status: 'completed', date: '2024-01-15' },
  { id: 2, customer: 'Bob', total: 120, status: 'cancelled', date: '2024-01-16' },
  { id: 3, customer: 'Alice', total: 350, status: 'completed', date: '2024-02-01' },
  { id: 4, customer: 'Charlie', total: 80, status: 'completed', date: '2024-02-10' },
  { id: 5, customer: 'Alice', total: 500, status: 'completed', date: '2024-03-05' },
  { id: 6, customer: 'Bob', total: 200, status: 'completed', date: '2024-03-15' },
  { id: 7, customer: 'Diana', total: 150, status: 'refunded', date: '2024-03-20' },
];

// Pipeline: completed orders → calculate revenue per customer → sort by revenue
const revenueByCustomer = orders
  .filter(order => order.status === 'completed')
  .map(order => ({ customer: order.customer, total: order.total }))
  .reduce((acc, { customer, total }) => {
    acc[customer] = (acc[customer] || 0) + total;
    return acc;
  }, {} as Record<string, number>);

console.log(revenueByCustomer);
// Output: { Alice: 1100, Charlie: 80, Bob: 200 }

// Convert to sorted array
const ranked = Object.entries(revenueByCustomer)
  .map(([customer, revenue]) => ({ customer, revenue }))
  .sort((a, b) => b.revenue - a.revenue);

console.log(ranked);
// Output:
// [
//   { customer: 'Alice', revenue: 1100 },
//   { customer: 'Bob', revenue: 200 },
//   { customer: 'Charlie', revenue: 80 },
// ]
// Another pipeline: process API response data
const apiResponse = [
  { id: 1, title: '  Hello World  ', tags: ['js', 'tutorial'], views: 1500 },
  { id: 2, title: '  Advanced TypeScript  ', tags: ['ts', 'advanced'], views: 800 },
  { id: 3, title: '  React Hooks Guide  ', tags: ['react', 'hooks'], views: 3200 },
  { id: 4, title: '  CSS Grid Tutorial  ', tags: ['css', 'layout'], views: 200 },
  { id: 5, title: '  Node.js Best Practices  ', tags: ['node', 'backend'], views: 2100 },
];

const result = apiResponse
  .filter(post => post.views >= 500)            // popular posts only
  .map(post => ({
    ...post,
    title: post.title.trim(),                    // clean up titles
    slug: post.title.trim().toLowerCase().replace(/[^a-z0-9]+/g, '-'),
    isViral: post.views > 2000,
  }))
  .reduce((acc, post) => {
    const category = post.isViral ? 'viral' : 'popular';
    if (!acc[category]) acc[category] = [];
    acc[category].push(post.title);
    return acc;
  }, {} as Record<string, string[]>);

console.log(result);
// Output:
// {
//   popular: ["Hello World", "Advanced TypeScript"],
//   viral: ["React Hooks Guide", "Node.js Best Practices"]
// }

9. flatMap() — 一步完成映射和展平

Array.prototype.flatMap()(ES2019)等同于先调用 map() 再调用 flat(1)。当你的映射函数为每个元素返回数组,并且你希望将结果展平为单个数组时非常有用。

// flatMap() = map() + flat(1)

// Split sentences into words
const sentences = ['Hello World', 'Foo Bar Baz', 'One Two'];
const words = sentences.flatMap(s => s.split(' '));
console.log(words);
// Output: ["Hello", "World", "Foo", "Bar", "Baz", "One", "Two"]

// Without flatMap (two steps):
const words2 = sentences.map(s => s.split(' ')).flat();
console.log(words2);
// Output: ["Hello", "World", "Foo", "Bar", "Baz", "One", "Two"]

// Use flatMap to conditionally expand or remove items
const nums = [1, 2, 3, 4, 5];

// Double even numbers, remove odd numbers
const result = nums.flatMap(n => n % 2 === 0 ? [n, n] : []);
console.log(result);
// Output: [2, 2, 4, 4]

// Expand ranges
const ranges = [[1, 3], [5, 7], [10, 12]];
const expanded = ranges.flatMap(([start, end]) => {
  const arr = [];
  for (let i = start; i <= end; i++) arr.push(i);
  return arr;
});
console.log(expanded);
// Output: [1, 2, 3, 5, 6, 7, 10, 11, 12]

// Extract all tags from posts
const posts = [
  { title: 'Post A', tags: ['js', 'react'] },
  { title: 'Post B', tags: ['ts', 'node'] },
  { title: 'Post C', tags: ['js', 'vue', 'css'] },
];

const allTags = posts.flatMap(p => p.tags);
console.log(allTags);
// Output: ["js", "react", "ts", "node", "js", "vue", "css"]

const uniqueTags = [...new Set(allTags)];
console.log(uniqueTags);
// Output: ["js", "react", "ts", "node", "vue", "css"]

10. for...of vs forEach vs map/filter/reduce 对比

每种迭代方式在性能、可读性和能力上都有取舍。以下是全面的对比,帮助你选择合适的工具。

// Comparison table:
// ┌─────────────────────┬───────────┬──────────┬───────────┬──────────────┐
// │ Feature             │ for...of  │ forEach  │ map       │ filter/reduce│
// ├─────────────────────┼───────────┼──────────┼───────────┼──────────────┤
// │ Returns value       │ No        │ No       │ Yes       │ Yes          │
// │ Chainable           │ No        │ No       │ Yes       │ Yes          │
// │ break/continue      │ Yes       │ No*      │ No        │ No           │
// │ async/await         │ Yes       │ No**     │ No**      │ No**         │
// │ Performance         │ Fastest   │ Fast     │ Fast      │ Fast         │
// │ Readability (intent)│ Low       │ Medium   │ High      │ High         │
// │ Mutates original    │ Can       │ Can      │ No        │ No           │
// └─────────────────────┴───────────┴──────────┴───────────┴──────────────┘
// * forEach: throwing is the only way to "break" — not recommended
// ** These work but require Promise.all() or special patterns

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

// 1. for...of — best when you need break/continue or async/await
let firstOver5: number | undefined;
for (const n of data) {
  if (n > 5) {
    firstOver5 = n;
    break; // early exit — impossible with map/filter/forEach
  }
}
console.log(firstOver5); // 6

// 2. forEach — for side effects only (logging, DOM updates)
data.forEach(n => console.log(n));
// Prints 1-10, returns undefined

// 3. map — when you want to TRANSFORM each element
const squares = data.map(n => n ** 2);
console.log(squares); // [1, 4, 9, 16, 25, 36, 49, 64, 81, 100]

// 4. filter — when you want to SELECT elements
const bigNums = data.filter(n => n > 7);
console.log(bigNums); // [8, 9, 10]

// 5. reduce — when you want to AGGREGATE into a single value
const sum = data.reduce((a, b) => a + b, 0);
console.log(sum); // 55

// 6. find() — like filter but returns only the FIRST match
const found = data.find(n => n > 5);
console.log(found); // 6

// 7. some() / every() — boolean checks
console.log(data.some(n => n > 9));  // true
console.log(data.every(n => n > 0)); // true
// Performance benchmark (rough comparison)
const bigArray = Array.from({ length: 1_000_000 }, (_, i) => i);

console.time('for...of');
let sum1 = 0;
for (const n of bigArray) sum1 += n;
console.timeEnd('for...of');
// ~3-5ms

console.time('forEach');
let sum2 = 0;
bigArray.forEach(n => { sum2 += n; });
console.timeEnd('forEach');
// ~5-8ms

console.time('reduce');
const sum3 = bigArray.reduce((a, b) => a + b, 0);
console.timeEnd('reduce');
// ~5-8ms

// Takeaway: the performance difference is negligible for most applications.
// Choose based on readability and intent, not micro-optimization.

11. TypeScript 中使用 map/filter/reduce

TypeScript 通过类型收窄泛型类型守卫增强了这些方法,在编译时而非运行时捕获错误。

// 11a. Type narrowing with filter()
// Problem: filter doesn't narrow types by default
const mixed: (string | number | null)[] = ['hello', 42, null, 'world', null, 7];

// ❌ TypeScript still thinks result is (string | number | null)[]
const noNulls = mixed.filter(x => x !== null);

// ✅ Use a type predicate (type guard) to narrow the type
const strings = mixed.filter((x): x is string => typeof x === 'string');
// TypeScript knows: strings is string[]
console.log(strings); // ["hello", "world"]

const numbers = mixed.filter((x): x is number => typeof x === 'number');
// TypeScript knows: numbers is number[]
console.log(numbers); // [42, 7]

// Reusable type guard
function isNotNull<T>(value: T | null | undefined): value is T {
  return value != null;
}

const cleaned = mixed.filter(isNotNull);
// TypeScript knows: cleaned is (string | number)[]
console.log(cleaned); // ["hello", 42, "world", 7]
// 11b. Generic reduce with proper types
interface CartItem {
  name: string;
  price: number;
  quantity: number;
}

const cart: CartItem[] = [
  { name: 'Laptop', price: 999, quantity: 1 },
  { name: 'Mouse', price: 29, quantity: 2 },
  { name: 'Cable', price: 15, quantity: 3 },
];

// TypeScript infers accumulator type from initial value
const totalPrice = cart.reduce((sum, item) => sum + item.price * item.quantity, 0);
console.log(totalPrice); // 1102

// Reduce to a different type — initial value determines type
interface Summary {
  itemCount: number;
  totalPrice: number;
  items: string[];
}

const summary = cart.reduce<Summary>(
  (acc, item) => ({
    itemCount: acc.itemCount + item.quantity,
    totalPrice: acc.totalPrice + item.price * item.quantity,
    items: [...acc.items, item.name],
  }),
  { itemCount: 0, totalPrice: 0, items: [] }
);

console.log(summary);
// Output: { itemCount: 6, totalPrice: 1102, items: ["Laptop", "Mouse", "Cable"] }
// 11c. Generic map utility with TypeScript
function mapBy<T, K extends keyof T>(arr: T[], key: K): T[K][] {
  return arr.map(item => item[key]);
}

interface Product {
  id: number;
  name: string;
  price: number;
}

const products: Product[] = [
  { id: 1, name: 'A', price: 10 },
  { id: 2, name: 'B', price: 20 },
];

const ids = mapBy(products, 'id');       // number[]
const names = mapBy(products, 'name');   // string[]
const prices = mapBy(products, 'price'); // number[]

console.log(ids);    // [1, 2]
console.log(names);  // ["A", "B"]
console.log(prices); // [10, 20]

// Generic groupBy with TypeScript
function groupBy<T>(arr: T[], keyFn: (item: T) => string): Record<string, T[]> {
  return arr.reduce<Record<string, T[]>>((acc, item) => {
    const key = keyFn(item);
    (acc[key] ??= []).push(item);
    return acc;
  }, {});
}

const byPrice = groupBy(products, p => p.price >= 15 ? 'expensive' : 'cheap');
console.log(byPrice);
// Output: { cheap: [{ id: 1, name: 'A', price: 10 }], expensive: [{ id: 2, name: 'B', price: 20 }] }

12. 常见错误及修复方法

即使是经验丰富的开发者也会掉入这些陷阱。以下是 map()filter()reduce() 最常见的错误及修复方法。

// Mistake 1: Forgetting to return in map() with curly braces
const nums = [1, 2, 3];

// ❌ WRONG: curly braces without return → returns undefined
const bad = nums.map(n => { n * 2 });
console.log(bad);
// Output: [undefined, undefined, undefined]

// ✅ FIX: add return statement
const good1 = nums.map(n => { return n * 2; });
console.log(good1); // [2, 4, 6]

// ✅ FIX: use concise arrow body (no curly braces)
const good2 = nums.map(n => n * 2);
console.log(good2); // [2, 4, 6]

// ⚠️ Returning an object literal? Wrap in parentheses!
// ❌ WRONG: JS thinks {} is a block
const bad2 = nums.map(n => { value: n });
// Output: [undefined, undefined, undefined]

// ✅ FIX: parentheses around the object
const good3 = nums.map(n => ({ value: n }));
console.log(good3); // [{ value: 1 }, { value: 2 }, { value: 3 }]
// Mistake 2: Missing initial value in reduce()

// ❌ Works but dangerous with empty arrays
const sum = [1, 2, 3].reduce((a, b) => a + b); // 6 — OK
// [].reduce((a, b) => a + b); // TypeError!

// ❌ Wrong type: accumulator starts as first element (number), not object
const items = [{ price: 10 }, { price: 20 }];
// items.reduce((acc, item) => acc + item.price); // NaN!
// Because acc starts as { price: 10 }, and { price: 10 } + 20 = NaN

// ✅ FIX: always provide initial value
const total = items.reduce((acc, item) => acc + item.price, 0);
console.log(total); // 30
// Mistake 3: Accidentally mutating objects inside map/filter

const users = [
  { name: 'Alice', score: 85 },
  { name: 'Bob', score: 92 },
];

// ❌ WRONG: mutating the original objects!
const bad = users.map(user => {
  user.grade = user.score >= 90 ? 'A' : 'B';  // mutates original!
  return user;
});
console.log(users[0]); // { name: 'Alice', score: 85, grade: 'B' } — MUTATED!

// ✅ FIX: create new objects with spread
const users2 = [
  { name: 'Alice', score: 85 },
  { name: 'Bob', score: 92 },
];
const good = users2.map(user => ({
  ...user,
  grade: user.score >= 90 ? 'A' : 'B',
}));
console.log(users2[0]); // { name: 'Alice', score: 85 } — unchanged
console.log(good[0]);   // { name: 'Alice', score: 85, grade: 'B' }
// Mistake 4: Using map() when you don't need the result

// ❌ WRONG: using map just for side effects, wasting memory
const data = [1, 2, 3, 4, 5];
data.map(n => console.log(n)); // creates an unused array of undefineds

// ✅ FIX: use forEach for side effects
data.forEach(n => console.log(n));

// Mistake 5: Chaining filter().map() when you can use reduce()
const nums2 = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

// ❌ Creates an intermediate array (wastes memory for large arrays)
const result1 = nums2
  .filter(n => n % 2 === 0)
  .map(n => n ** 2);

// ✅ Single pass with reduce (better for very large arrays)
const result2 = nums2.reduce<number[]>((acc, n) => {
  if (n % 2 === 0) acc.push(n ** 2);
  return acc;
}, []);

console.log(result1); // [4, 16, 36, 64, 100]
console.log(result2); // [4, 16, 36, 64, 100]
// Note: filter+map is more readable; use reduce only if performance matters

13. 常见问题

JavaScript 中 map() 和 forEach() 有什么区别?

map() 返回一个包含转换后元素的新数组,而 forEach() 返回 undefined,仅用于执行副作用。当你需要结果数组时使用 map();当你只需要执行操作(如日志记录或 DOM 更新)而不收集结果时使用 forEach()。map() 可以链式调用,forEach() 不行。

什么时候应该用 reduce() 而不是 for 循环?

当你需要将数组聚合为单个值(求和、对象、字符串等)且逻辑简洁时,使用 reduce()。对于需要提前退出、副作用或异步操作的复杂多步逻辑,for 循环可能更清晰。reduce() 在 groupBy、计数、展平和函数组合等模式中表现出色。

filter() 会修改原数组吗?

不会。filter() 总是返回新数组,原数组保持不变。但如果数组包含对象,新数组持有对相同对象的引用 — 因此修改过滤后数组中的对象也会影响原数组。要避免这种情况,可以将 filter() 与 map() 和展开运算符/Object.assign 结合使用来创建浅拷贝。

如果忘记给 reduce() 提供初始值会怎样?

如果省略初始值,reduce() 将第一个元素作为初始累加器,从第二个元素开始迭代。这对简单的数值求和有效,但在空数组上会抛出 TypeError,且当累加器类型与元素类型不同时(如归约为对象)会产生错误结果。务必提供初始值。

可以将 map()、filter() 和 reduce() 与 async/await 一起使用吗?

带 async 回调的 map() 返回 Promise 数组 — 使用 Promise.all(arr.map(async fn)) 来等待所有结果。filter() 不能直接与 async 回调配合,因为 Promise 总是真值;需要先 map 获取布尔结果,然后再 filter。reduce() 可以通过在累加器上使用 await 来链式处理异步操作:arr.reduce(async (accP, item) => { const acc = await accP; ... }, Promise.resolve(initial))。

掌握这三个方法,你将写出更简洁、更易维护的 JavaScript 代码。要快速检查数据,试试下面的工具。

试试 JSON 格式化工具 →

试试 JS/HTML 格式化工具 →

𝕏 Twitterin LinkedIn
这篇文章有帮助吗?

保持更新

获取每周开发技巧和新工具通知。

无垃圾邮件,随时退订。

试试这些相关工具

{ }JSON FormatterJSJS/HTML Formatter.*Regex Tester

相关文章

JavaScript 数组方法速查表

JavaScript 数组方法完整参考:map、filter、reduce、find、some、every、flat、splice、slice 等,附清晰示例。

JavaScript 字符串正则替换:replaceAll、捕获组与示例

掌握 JavaScript 中使用正则表达式的字符串替换。学习 replace 与 replaceAll、全局标志、捕获组、前瞻断言和实际示例。