功能说明
该示例把列表聚合为分组结构,返回 Map 而不是普通对象,因此键可以是数字、布尔值、对象引用或 Symbol,且不会与原型属性冲突。keyOf 接收元素与下标,valueOf 可把元素投影成统计值或精简对象。groupByProperty 是读取固定字段的便捷封装。适用于按状态归类订单、按作者聚合文章、构建索引等数据处理任务。
关键代码
实现只遍历一次,按首次出现的键顺序插入 Map;重复键直接追加到已有数组。桶数组在首次遇到键时创建,避免为不存在的分组预分配。keyOf 与 valueOf 都会收到原始下标,便于在投影时保留位置信息。groupByProperty 对 null 或 undefined 元素使用 undefined 作为键,而不是直接访问属性导致 TypeError,使脏数据仍可被归入单独分组。
使用注意
Map 的键按 SameValueZero 比较,NaN 被视为同一个键,对象键按引用而不是结构相等,因此内容相同的两个对象会落入不同分组。返回的桶数组是新建的,但元素仍是原引用,修改元素会影响原数组。分组顺序是首次出现顺序,不是键的字典序。需要普通对象输出时应在消费端显式转换,并注意特殊键名。
验证方式
按字符串字段分组,断言 Map 的键顺序与首次出现顺序一致;用同一对象引用作为键验证只分一组;测试数字、布尔值与 NaN 键;使用 valueOf 投影只保留 id,断言桶内元素被替换;传入含 null 的数组调用 groupByProperty,确认不会抛错且落到 undefined 分组;测试非数组输入抛 TypeError。
代码示例
export function groupBy(items, keyOf, valueOf = (item) => item) {
if (!Array.isArray(items)) throw new TypeError("items must be an array");
if (typeof keyOf !== "function") throw new TypeError("keyOf must be a function");
if (typeof valueOf !== "function") throw new TypeError("valueOf must be a function");
const groups = new Map();
for (let index = 0; index < items.length; index += 1) {
const item = items[index];
const key = keyOf(item, index);
let bucket = groups.get(key);
if (!bucket) {
bucket = [];
groups.set(key, bucket);
}
bucket.push(valueOf(item, index));
}
return groups;
}
export function groupByProperty(items, property) {
if (typeof property !== "string" || property.length === 0) {
throw new TypeError("property must be a non-empty string");
}
return groupBy(items, (item) => (item == null ? undefined : item[property]));
}