JavaScript 的 map()、filter() 和 reduce() 是函数式数组处理的三大支柱。它们让你在不修改原数组的情况下进行数据转换、筛选和聚合。结合方法链式调用,它们能构建出比命令式循环更易读、易测试、易维护的数据处理管道。本指南深入讲解每个方法,配有可运行的代码示例,展示输入和输出结果。
使用我们的 JSON 格式化工具检查和格式化你的 JSON 数据 →
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] — unchanged2. 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); // 07. 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 = 108. 链式调用 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 matters13. 常见问题
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 代码。要快速检查数据,试试下面的工具。