数组与高阶函数
前言
上一篇我们学了对象和原型链。现在来看 JS 中最常用的数据结构——数组。
前端开发中几乎每个页面都涉及列表渲染,而列表数据就是数组。在 C 里你用 for 循环遍历数组,手动管理索引和边界。JS 的数组提供了一套强大的高阶函数(map/filter/reduce 等),让你用“声明式”的方式处理数据——告诉程序“做什么”而不是“怎么做”。
本文的重点是 reduce——数组方法中的“万能工具”,以及 不可变更新模式——React 开发的必备技能。
正文
1. 数组基础
const arr = [1, 2, 3, 4, 5];
const mixed = [1, "hello", true, null, { name: "Alice" }];
const filled = new Array(5).fill(0); // [0, 0, 0, 0, 0]
arr[0]; // 1
arr[arr.length - 1]; // 5(最后一个元素)
Array.isArray(arr); // true
2. 常用增删方法
const fruits = ["apple", "banana"];
// 尾部操作
fruits.push("cherry"); // ["apple", "banana", "cherry"]
fruits.pop(); // 返回 "cherry"
// 头部操作
fruits.unshift("mango"); // ["mango", "apple", "banana"]
fruits.shift(); // 返回 "mango"
// splice:万能增删改(修改原数组)
const nums = [1, 2, 3, 4, 5];
nums.splice(2, 1); // 从索引2删1个 → [1, 2, 4, 5]
nums.splice(1, 0, 10, 20); // 从索引1插入 → [1, 10, 20, 2, 4, 5]
// slice:截取(不修改原数组)
[1, 2, 3, 4, 5].slice(1, 3); // [2, 3]
[1, 2, 3, 4, 5].slice(-2); // [4, 5]
// 其他常用
[1, 2].concat([3, 4]); // [1, 2, 3, 4]
[1, 2, 3].includes(2); // true
[1, 2, 3].indexOf(2); // 1
[3, 1, 4].sort((a, b) => a - b); // [1, 3, 4]
[1, 2, 3].reverse(); // [3, 2, 1]
["a", "b"].join("-"); // "a-b"
[1, [2, [3]]].flat(Infinity); // [1, 2, 3]
3. 高阶函数
3.1 forEach — 遍历
["apple", "banana", "cherry"].forEach((fruit, index) => {
console.log(`${index}: ${fruit}`);
});
3.2 map — 映射转换
对每个元素操作,返回新数组(不修改原数组):
const numbers = [1, 2, 3, 4, 5];
const doubled = numbers.map(n => n * 2); // [2, 4, 6, 8, 10]
const users = [
{ name: "Alice", age: 25 },
{ name: "Bob", age: 30 },
];
const names = users.map(u => u.name); // ["Alice", "Bob"]
3.3 filter — 筛选
返回满足条件的元素组成的新数组:
const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const evens = numbers.filter(n => n % 2 === 0); // [2, 4, 6, 8, 10]
// 删除假值
[0, 1, "", "hello", null, 42].filter(Boolean); // [1, "hello", 42]
3.4 ★ reduce — 归约(万能工具)
reduce 是数组方法中最强大也最难理解的一个。它的核心思想是:带着一个“累加器”遍历数组,每步更新累加器,最后返回累加器的值:
const numbers = [1, 2, 3, 4, 5];
// 求和
const sum = numbers.reduce((acc, cur) => acc + cur, 0); // 15
// 求最大值
const max = numbers.reduce((m, n) => n > m ? n : m, -Infinity); // 5
// 统计词频
const words = ["hello", "world", "hello", "js", "hello"];
const freq = words.reduce((acc, w) => {
acc[w] = (acc[w] || 0) + 1;
return acc;
}, {});
// { hello: 3, world: 1, js: 1 }
// 事实上,map 和 filter 都可以用 reduce 实现——它是数组方法的“底层 API”
💡 工程师手记:
reduce是我学 JS 时觉得最“神奇”的方法。刚开始看不懂,后来发现它的本质就是一个带着“状态”遍历的 for 循环——acc就是那个状态。用嵌入式的话说,就像一个状态机,每次处理一个输入就更新一次状态。(建议替换为你自己理解 reduce 的顿悟时刻)
3.5 find / findIndex
const users = [
{ id: 1, name: "Alice" },
{ id: 2, name: "Bob" },
];
users.find(u => u.id === 2); // { id: 2, name: "Bob" }
users.findIndex(u => u.id === 2); // 1
3.6 some / every
[1, 3, 5, 7, 8].some(n => n % 2 === 0); // true(8是偶数)
[1, 3, 5, 7].every(n => n % 2 === 1); // true(全是奇数)
4. 链式调用
const products = [
{ name: "Phone", price: 999, inStock: true },
{ name: "Laptop", price: 1999, inStock: true },
{ name: "Tablet", price: 599, inStock: false },
{ name: "Watch", price: 399, inStock: true },
];
const result = products
.filter(p => p.inStock)
.filter(p => p.price >= 500)
.sort((a, b) => a.price - b.price)
.map(p => `${p.name} ¥${p.price}`);
// ["Phone ¥999", "Laptop ¥1999"]
5. 展开运算符与解构
// 展开
const merged = [...[1, 2], ...[3, 4]]; // [1, 2, 3, 4]
Math.max(...[3, 1, 4]); // 4
// 解构
const [first, second, ...rest] = [1, 2, 3, 4, 5];
// first=1, second=2, rest=[3,4,5]
// 交换变量
let a = 1, b = 2;
[a, b] = [b, a]; // a=2, b=1
6. 不可变更新模式(React 必备)
在 React 中,状态更新必须是“不可变”的——不能直接修改原数组,而是创建一个新数组。这是因为 React 通过对比“新旧状态是否是同一个引用”来决定是否重新渲染,如果你直接修改原数组,引用没变,React 会认为“没变化”而跳过渲染。
const todos = [
{ id: 1, text: "学HTML", done: true },
{ id: 2, text: "学CSS", done: true },
{ id: 3, text: "学JS", done: false },
];
// 添加(不用 push)
const added = [...todos, { id: 4, text: "学React", done: false }];
// 删除(不用 splice)
const removed = todos.filter(t => t.id !== 2);
// 更新某一项(不直接修改)
const updated = todos.map(t =>
t.id === 3 ? { ...t, done: true } : t
);
总结
| 知识点 | 核心要点 |
|---|---|
| map | 转换每个元素,返回新数组 |
| filter | 筛选满足条件的元素 |
| reduce | 归约为单个值(求和、统计、分组) |
| find | 查找第一个满足条件的元素 |
| some/every | 条件检测 |
| 链式调用 | filter → sort → map 数据管道 |
| 展开/解构 | [...arr] 浅拷贝、[a, b] = arr 提取 |
| 不可变更新 | React 中用展开/filter/map 代替 push/splice |
常见问题
💬 你可能会问:map/filter 和 for 循环有什么区别?性能差异大吗?
功能上等价,但 map/filter 更简洁、更不容易出错(不用管理索引)。性能差异微乎其微,前端场景下完全可以忽略。优先用高阶函数,代码可读性更重要。
💬 你可能会问:reduce 太难理解了,可以不用吗?
日常开发中,求和、统计、分组等场景确实需要 reduce。但如果你觉得 reduce 读起来很吃力,用 for 循环实现同样的逻辑也完全没问题——可读性永远优先于“炒作”。
💬 你可能会问:为什么 React 要求不可变更新?直接 push 不行吗?
React 通过对比“新旧状态是否是同一个引用”来决定是否重新渲染。直接 push 修改原数组,引用没变,React 会认为“没变化”而跳过更新。后续学 React 时你会深切体会这一点。
下一步行动:在 Console 中练习链式调用——给一个产品数组依次应用 filter、sort、map,试试用 reduce 实现求和和词频统计。
参考资料
📖 系列导航:本文是「FPGA 工程师的前端学习笔记」系列的第 9 篇 上一篇:对象与原型链 下一篇:DOM 操作与事件机制