JavaScript 教程实战:用 reduce 实现 getAverageAge 计算对象数组的平均年龄
JavaScript 教程实战:用 reduce 实现 getAverageAge 计算对象数组的平均年龄
导读
本篇以 Modern JavaScript Tutorial(en.javascript.info)仓库中的经典习题"Get average age"(任务描述)为线索,讲解如何对一个由 {name, age} 对象组成的数组使用 Array.prototype.reduce 求和并除以 length 得到平均年龄。你将掌握 reduce 累加器的正确用法、为空数组保留健壮性的写法,以及将"求平均"推广到"求总和/最大值/分组"等常见聚合场景的实战方法,并看到它在 数组方法章节 中的位置与原理支撑。
一、题目本身:从对象数组中求平均年龄
题目要求编写函数 getAverageAge(users),它接收一个带有 age 属性的对象数组,返回所有人的平均年龄。平均值的公式为:
(age1 + age2 + ... + ageN) / N
其中 N 是数组中对象的个数。给定示例数据:
let john = { name: "John", age: 25 };
let pete = { name: "Pete", age: 30 };
let mary = { name: "Mary", age: 29 };
let arr = [ john, pete, mary ];
alert( getAverageAge(arr) ); // (25 + 30 + 29) / 3 = 28
这是 05-array-methods 章节末尾的一道综合练习(importance 4),考察的核心是"遍历数组 → 累加每个元素的某个属性 → 得到一个标量结果",而这正是 reduce 的典型使用场景。
二、官方参考解法:一行 reduce 完成求和
仓库中 solution.md 给出的参考解法非常精炼:
function getAverageAge(users) {
return users.reduce((prev, user) => prev + user.age, 0) / users.length;
}
let john = { name: "John", age: 25 };
let pete = { name: "Pete", age: 30 };
let mary = { name: "Mary", age: 29 };
let arr = [ john, pete, mary ];
alert( getAverageAge(arr) ); // 28
拆解这一行代码:
users.reduce((prev, user) => prev + user.age, 0):从初始值0开始,逐个对象累加user.age,最终得到年龄总和25 + 30 + 29 = 84;/ users.length:除以人数3,得到平均年龄28。
这里 reduce 的回调只用到两个参数:prev(累加器,上一次调用的返回值)和 user(当前数组元素)。reduce 剩余两个可选参数 index 与 array 在此场景下并不需要。
reduce 的完整签名与执行过程
在 article.md 的 "reduce/reduceRight" 小节中,官方给出了完整语法:
let value = arr.reduce(function(accumulator, item, index, array) {
// ...
}, [initial]);
accumulator—— 上一次函数调用的结果,首次调用时等于initial(若提供了初值);item—— 当前数组元素;index—— 当前元素位置;array—— 数组本身。
以 [25, 30, 29](即依次取 user.age)配合初始值 0 为例,调用过程如下表:
| 调用次序 | sum(累加器) |
current |
返回结果 |
|---|---|---|---|
| 第 1 次 | 0 |
25 |
25 |
| 第 2 次 | 25 |
30 |
55 |
| 第 3 次 | 55 |
29 |
84 |
最终 84 成为 reduce 的返回值,再除以 3 即得平均年龄。
三、为什么初始值 0 必不可少
reduce 允许省略初始值:此时它会把数组第一个元素当作累加器的初值,并从第二个元素开始迭代。对求和来说结果通常一致,但存在一个致命边界:
let arr = [];
// Error: Reduce of empty array with no initial value
arr.reduce((sum, current) => sum + current);
当数组为空且未提供初始值时,reduce 会抛出 TypeError。因此 article.md 明确建议:始终显式指定初始值。getAverageAge 中的 , 0 正是这一最佳实践的体现——即使传入空数组,reduce 也会返回 0(只是再除以 0 会得到 NaN,若需完全健壮可加空数组判断,见下文扩展)。
四、让函数更健壮:处理空数组与缺失 age
生产环境中数据未必总是"规整",可以对官方解法做防御性增强:
function getAverageAge(users) {
if (users.length === 0) return 0; // 避免 0/0 得到 NaN
const total = users.reduce((sum, user) => sum + (user.age || 0), 0);
return total / users.length;
}
- 空数组提前返回
0,避免0 / 0 = NaN; user.age || 0忽略undefined/null等缺失年龄字段,保证求和不会中途变成NaN。
五、从"平均"到通用聚合:reduce 的进阶用法
reduce 的价值不止于求和。同章节的练习 Create keyed object from array 要求用 reduce 把用户数组按 id 聚合成对象,这正是 getAverageAge 所用技巧的延伸:
function groupById(arr) {
return arr.reduce((obj, user) => {
obj[user.id] = user;
return obj;
}, {});
}
同样的思路可以轻松迁移到其他聚合场景:
// 最大年龄
const maxAge = users.reduce((max, u) => Math.max(max, u.age), -Infinity);
// 年龄总和
const totalAge = users.reduce((sum, u) => sum + u.age, 0);
// 各年龄段人数统计
const byRange = users.reduce((acc, u) => {
const key = u.age < 30 ? 'under30' : 'over30';
acc[key] = (acc[key] || 0) + 1;
return acc;
}, {});
可以看到,reduce 的第二个参数(初始值)决定了"聚合结果的形态":0 是数值累加,{} 是按键分组,[] 则可用于收集满足条件的元素。
六、小结
- 求对象数组的平均年龄 =
reduce累加某属性 + 除以users.length; - 始终为
reduce提供初始值,避免空数组抛错; - 回调只需关心
accumulator与item两个参数,即可完成求和、求最值、分组等绝大多数聚合任务; - 完整解法与逐步推导可对照 solution.md 与 数组方法章节 学习。