
在日常开发中,我们经常会遇到需要从复杂数据结构中提取并聚合特定信息的需求。例如,给定一个包含多个对象的数组,每个对象又包含一个嵌套的数组,我们的目标可能是统计嵌套数组中元素的出现频率。以一个nfl球队数据为例:
var nflTeams = [
{ name: 'Kansas City Chiefs', playersFirstNames: ['Shane', 'Chad', 'Michael', 'Ronald', 'Blake', 'Noah'], champions: true },
{ name: 'Philadelphia Eagles', playersFirstNames: ['Jalen', 'Kenneth', 'Boston', 'Trey', 'Jack', 'Andre', 'Jack', 'Lane', 'Jason', 'Nakobe'], champions: false },
{ name: 'Cincinnati Bengals', playersFirstNames: ['Brandon', 'Joe', 'Chris', 'Joe', 'Tyler', 'Trenton', 'Trent', 'Mitchell', 'Alex', 'Trey', 'Ted'], champions: false },
{ name: 'San Francisco 49ers', playersFirstNames: ['Jimmy', 'Josh', 'Kyle', 'Jordan', 'Brandon', 'Danny', 'George', 'Tyler', 'Charlie', 'Jake', 'Nick', 'Nick', 'Kevin'], champions: false },
];我们的目标是从所有球队中提取所有球员的名字,并统计每个名字出现的次数,最终生成一个类似 {'Joe': 2, 'Jimmy': 1, 'Jack': 2, ...} 的对象。Underscore.js 提供了多种方法来优雅地解决这类问题。
对于计数频率这类特定需求,Underscore.js 提供了 _.countBy() 方法,它专门用于根据给定迭代器(函数或属性名)对集合中的元素进行分组并计数。这是实现我们目标最简洁和高效的方法。
在使用 _.countBy() 之前,我们需要将所有球队的 playersFirstNames 数组合并成一个单一的、扁平化的球员名字列表。
使用原生 JavaScript 的 Array.prototype.flatMap() (如果环境支持) 如果你的开发环境支持 ES2019 及以上版本的 JavaScript,flatMap() 是一个非常方便的选择,它结合了 map 和 flatten 的功能。
const allPlayerNames = nflTeams.flatMap(team => team.playersFirstNames); // console.log(allPlayerNames); // 输出一个包含所有球员名字的扁平数组
使用 Underscore.js 的 _.map() 和 _.flatten() 链式调用 这是更符合 Underscore.js 链式调用风格的方式,也是在不支持 flatMap() 环境下的通用做法。
const allPlayerNames = _.chain(nflTeams)
.map('playersFirstNames') // 提取所有球队的 playersFirstNames 数组
.flatten() // 将数组的数组扁平化成一个单一数组
.value(); // 获取链式操作的最终结果
// console.log(allPlayerNames); // 输出一个包含所有球员名字的扁平数组一旦我们有了扁平化的球员名字列表,就可以直接将它传递给 _.countBy()。由于我们想要统计每个名字本身的出现次数,_.countBy() 不需要额外的迭代器函数,它会默认将数组中的每个元素作为键进行计数。
完整代码示例(结合 flatMap() 和 _.countBy()):
const nflTeams = [
{ name: 'Kansas City Chiefs', playersFirstNames: ['Shane', 'Chad', 'Michael', 'Ronald', 'Blake', 'Noah'], champions: true },
{ name: 'Philadelphia Eagles', playersFirstNames: ['Jalen', 'Kenneth', 'Boston', 'Trey', 'Jack', 'Andre', 'Jack', 'Lane', 'Jason', 'Nakobe'], champions: false },
{ name: 'Cincinnati Bengals', playersFirstNames: ['Brandon', 'Joe', 'Chris', 'Joe', 'Tyler', 'Trenton', 'Trent', 'Mitchell', 'Alex', 'Trey', 'Ted'], champions: false },
{ name: 'San Francisco 49ers', playersFirstNames: ['Jimmy', 'Josh', 'Kyle', 'Jordan', 'Brandon', 'Danny', 'George', 'Tyler', 'Charlie', 'Jake', 'Nick', 'Nick', 'Kevin'], champions: false },
];
const nameOccurrences = _.countBy(nflTeams.flatMap(team => team.playersFirstNames));
console.log(nameOccurrences);
/*
输出:
{
Shane: 1, Chad: 1, Michael: 1, Ronald: 1, Blake: 1, Noah: 1,
Jalen: 1, Kenneth: 1, Boston: 1, Trey: 2, Jack: 2, Andre: 1, Lane: 1, Jason: 1, Nakobe: 1,
Brandon: 2, Joe: 2, Chris: 1, Tyler: 2, Trenton: 1, Trent: 1, Mitchell: 1, Alex: 1, Ted: 1,
Jimmy: 1, Josh: 1, Kyle: 1, Jordan: 1, Danny: 1, George: 1, Charlie: 1, Jake: 1, Nick: 2, Kevin: 1
}
*/完整代码示例(结合 Underscore 链式调用 map().flatten().countBy()):
const nflTeams = [
{ name: 'Kansas City Chiefs', playersFirstNames: ['Shane', 'Chad', 'Michael', 'Ronald', 'Blake', 'Noah'], champions: true },
{ name: 'Philadelphia Eagles', playersFirstNames: ['Jalen', 'Kenneth', 'Boston', 'Trey', 'Jack', 'Andre', 'Jack', 'Lane', 'Jason', 'Nakobe'], champions: false },
{ name: 'Cincinnati Bengals', playersFirstNames: ['Brandon', 'Joe', 'Chris', 'Joe', 'Tyler', 'Trenton', 'Trent', 'Mitchell', 'Alex', 'Trey', 'Ted'], champions: false },
{ name: 'San Francisco 49ers', playersFirstNames: ['Jimmy', 'Josh', 'Kyle', 'Jordan', 'Brandon', 'Danny', 'George', 'Tyler', 'Charlie', 'Jake', 'Nick', 'Nick', 'Kevin'], champions: false },
];
const nameOccurrences = _.chain(nflTeams)
.map('playersFirstNames')
.flatten()
.countBy()
.value();
console.log(nameOccurrences);
/*
输出:
{
Shane: 1, Chad: 1, Michael: 1, Ronald: 1, Blake: 1, Noah: 1,
Jalen: 1, Kenneth: 1, Boston: 1, Trey: 2, Jack: 2, Andre: 1, Lane: 1, Jason: 1, Nakobe: 1,
Brandon: 2, Joe: 2, Chris: 1, Tyler: 2, Trenton: 1, Trent: 1, Mitchell: 1, Alex: 1, Ted: 1,
Jimmy: 1, Josh: 1, Kyle: 1, Jordan: 1, Danny: 1, George: 1, Charlie: 1, Jake: 1, Nick: 2, Kevin: 1
}
*/这两种方法都非常简洁高效,特别是第二种,完全符合使用 Underscore.js 链式调用的初衷。
尽管 _.countBy() 是此场景下的最佳选择,但理解如何正确使用 _.reduce() 对于处理更复杂的聚合逻辑至关重要。_.reduce() 是一个非常强大的函数,它通过对集合中的每个元素应用一个回调函数,将集合“缩减”为单个值(例如一个对象、一个数组或一个数字)。
在尝试使用 _.reduce() 统计名字出现次数时,常见的错误是回调函数中的逻辑问题,尤其是在处理赋值和条件判断时。例如,一个常见的错误尝试可能如下:
// 错误的 reduce 尝试
var firstNameOccurence = _.chain (nflTeams)
.map(function(team) {return team.playersFirstNames})
.flatten()
.reduce(function(newObject, firstName){
// 错误逻辑:newObject[firstName] = 1 ? !newObject[firstName] : newObject[firstName] += 1;
// 解释:
// 1. `newObject[firstName] = 1` 是一个赋值操作,它的结果是赋的值(即 1)。
// 2. `1` 在布尔上下文中为真,所以三元表达式的条件 `newObject[firstName] = 1` 始终为真。
// 3. 结果是,回调函数将始终执行三元表达式的真分支:`!newObject[firstName]`。
// 4. `newObject[firstName]` 刚被赋值为 1,所以 `!newObject[firstName]` 实际上是 `!1`,即 `false`。
// 5. 因此,`reduce` 在第一次迭代后会返回 `false`,后续迭代中 `newObject` 不再是对象,导致错误或非预期行为。
return newObject[firstName] = 1 ? !newObject[firstName] : newObject[firstName] += 1;
}, {})
.value();
// console.log(firstNameOccurence); // 结果将是 true这个例子清晰地展示了对 JavaScript 核心运算符(如赋值 =、逻辑非 ! 和三元运算符 ? :)理解不足可能导致的错误。reduce 回调函数必须始终返回累加器(这里是 newObject),而不是一个布尔值或其他非预期的类型。
要正确使用 _.reduce() 来统计元素频率,我们需要确保:
正确的 _.reduce() 回调函数逻辑应该像这样:
const nflTeams = [
{ name: 'Kansas City Chiefs', playersFirstNames: ['Shane', 'Chad', 'Michael', 'Ronald', 'Blake', 'Noah'], champions: true },
{ name: 'Philadelphia Eagles', playersFirstNames: ['Jalen', 'Kenneth', 'Boston', 'Trey', 'Jack', 'Andre', 'Jack', 'Lane', 'Jason', 'Nakobe'], champions: false },
{ name: 'Cincinnati Bengals', playersFirstNames: ['Brandon', 'Joe', 'Chris', 'Joe', 'Tyler', 'Trenton', 'Trent', 'Mitchell', 'Alex', 'Trey', 'Ted'], champions: false },
{ name: 'San Francisco 49ers', playersFirstNames: ['Jimmy', 'Josh', 'Kyle', 'Jordan', 'Brandon', 'Danny', 'George', 'Tyler', 'Charlie', 'Jake', 'Nick', 'Nick', 'Kevin'], champions: false },
];
const nameOccurrencesWithReduce = _.chain(nflTeams)
.map('playersFirstNames')
.flatten()
.reduce((currObject, firstName) => {
// 如果 currObject 中不存在 firstName,则将其初始化为 0,然后加 1
// 否则,直接将其值加 1
currObject[firstName] = (currObject[firstName] || 0) + 1;
return currObject; // 始终返回累加器对象
}, {}) // 初始累加器是一个空对象
.value();
console.log(nameOccurrencesWithReduce);
/*
输出:
{
Shane: 1, Chad: 1, Michael: 1, Ronald: 1, Blake: 1, Noah: 1,
Jalen: 1, Kenneth: 1, Boston: 1, Trey: 2, Jack: 2, Andre: 1, Lane: 1, Jason: 1, Nakobe: 1,
Brandon: 2, Joe: 2, Chris: 1, Tyler: 2, Trenton: 1, Trent: 1, Mitchell: 1, Alex: 1, Ted: 1,
Jimmy: 1, Josh: 1, Kyle: 1, Jordan: 1, Danny: 1, George: 1, Charlie: 1, Jake: 1, Nick: 2, Kevin: 1
}
*/在这个修正后的 reduce 实现中,currObject[firstName] = (currObject[firstName] || 0) + 1; 确保了当 firstName 第一次出现时,其计数被初始化为 1;之后每次出现,计数都会递增。关键在于,我们每次都返回了 currObject,保证了 reduce 的累加过程是正确的。
关于效率的额外说明: 虽然上述 reduce 方法有效,但如果每次迭代都创建一个新对象 ({...currObject, [firstName]: (currObject[firstName] || 0) + 1}),虽然更具函数式编程风格,但在处理大量数据时,其性能会低于直接修改 currObject 的方式,因为它会创建大量中间对象。在 Underscore.js 的 reduce 场景下,直接修改累加器是常见且高效的做法。
通过本文的介绍,您应该能够熟练地使用 Underscore.js 处理嵌套数组并统计元素出现次数,并理解在不同场景下选择 _.countBy() 或 _.reduce() 的最佳实践。
以上就是如何使用 Underscore.js 处理嵌套数组并统计元素出现次数的详细内容,更多请关注php中文网其它相关文章!
每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号