es-toolkit 的 Map 版 keyBy:用键生成函数高效重组 Map 数据结构的实战指南
【免费下载链接】es-toolkitA modern JavaScript utility library that's 2-3 times faster and up to 97% smaller, a major upgrade to lodash.项目地址: https://gitcode.com/GitHub_Trending/es/es-toolkit
keyBy是 es-toolkit 在map模块中提供的一个实用函数:给定一个Map和一个键生成函数getKeyFromEntry,它会遍历原 Map 的每一个条目,用该函数为每个条目生成新键,最终返回一个"新键 → 原值"的全新Map。在按 ID、角色、分类等业务字段为 Map 数据建立索引、去重归组或重写键名等场景中,它都能让你用一行代码完成原本需要手写循环的工作。读完本文,你将掌握keyBy(map, getKeyFromEntry)的完整签名、参数与返回值语义、多个可直接运行的实操示例,以及它的源码级实现原理与测试覆盖情况。
功能概述:一个函数重写整个 Map 的键
keyBy解决的问题非常明确:当你手头的数据以Map形式存储,但现有键(比如'x'、'user1'、'item_1'这类内部标识符)并不适合后续查找,而你希望基于每个条目本身的内容重新生成一套键时,keyBy就是为此设计的。
它的调用形式极其简洁:
const result = keyBy(map, getKeyFromEntry);需要注意的一个关键设计是:这个函数只能从es-toolkit/map子路径导入,而不是从es-toolkit主入口导入。官方文档特别说明,这是为了避免与其他集合类型(如数组)中的同名函数keyBy产生潜在冲突——实际上 es-toolkit 在 src/array/keyBy.ts 中确实提供了针对数组的keyBy,二者行为相似但返回类型不同(数组版返回普通对象Record,Map 版返回Map)。因此在使用时请务必写清楚子路径:
import { keyBy } from 'es-toolkit/map';基本用法:按值属性重新索引 Map
文档中的第一个示例展示了最典型的应用场景:把一个以单字符为键的 Map,按照条目内部type字段重新组织。
import { keyBy } from 'es-toolkit/map'; const map = new Map([ ['x', { type: 'fruit', name: 'apple' }], ['y', { type: 'fruit', name: 'banana' }], ['z', { type: 'vegetable', name: 'carrot' }], ]); const result = keyBy(map, item => item.type); // 结果: // Map(2) { // 'fruit' => { type: 'fruit', name: 'banana' }, // 'vegetable' => { type: 'vegetable', name: 'carrot' } // } // 注意: 'banana' 被保留,因为它是最后一个遇到的 'fruit'这里有两个要点值得强调:
- 新键来自值的内容:原 Map 中
'x'、'y'两个条目的值都属于fruit类型,因此它们被归并到同一个新键'fruit'下,Map的大小从 3 缩小为 2。 - 同键冲突时"后者胜出":当多个条目生成了相同的新键时,最后被遍历到的那个值会覆盖之前的值。这正是示例中
'banana'取代'apple'的原因。
多场景实践:按各种业务标准重组数据
keyBy的键生成函数非常灵活,你可以基于任意规则来重组数据。下面是文档中给出的三组典型场景。
按 ID 属性建立精确索引
当值对象带有唯一标识(如用户 ID)时,keyBy可以把它提升为 Map 的键,让后续的map.get(101)查找变得直接高效:
import { keyBy } from 'es-toolkit/map'; const users = new Map([ ['user1', { id: 101, name: 'Alice', role: 'admin' }], ['user2', { id: 102, name: 'Bob', role: 'user' }], ['user3', { id: 103, name: 'Charlie', role: 'user' }], ]); const byId = keyBy(users, user => user.id); // 结果: 键为 101、102、103 的 Map按分类字段归组(每组保留最后一个)
与精确索引不同,当多个条目共享同一个分类值时,keyBy会执行"按分类归组、同组取最后一个"的语义:
const byRole = keyBy(users, user => user.role); // 结果: Map(2) { // 'admin' => { id: 101, name: 'Alice', role: 'admin' }, // 'user' => { id: 103, name: 'Charlie', role: 'user' } // }可以看到,user2和user3都属于'user'角色,由于user3后出现,最终'user'键下保留的是 Charlie。
同时利用值和原键生成复合键
键生成函数并非只能访问值,它还能拿到原 Map 的键以及原 Map 本身,因此可以构造出包含两者信息的复合键:
const inventory = new Map([ ['item_1', { category: 'electronics', price: 100 }], ['item_2', { category: 'electronics', price: 200 }], ]); const categorized = keyBy(inventory, (value, key) => `${value.category}_${key}`); // 结果: 键为 'electronics_item_1'、'electronics_item_2' 的 Map这种"值 + 原键"拼接的方式,常用于需要保留原始标识信息的同时又按业务维度区分的场景。
参数与返回值详解
依据 keyBy 文档 与 源码定义,完整的签名如下:
keyBy<K, V, K2>( map: Map<K, V>, getKeyFromEntry: (value: V, key: K, object: Map<K, V>) => K2 ): Map<K2, V>参数(Parameters)
| 参数 | 类型 | 说明 |
|---|---|---|
map | Map<K, V> | 要被重新映射的条目集合,即原 Map 本身 |
getKeyFromEntry | (value: V, key: K, object: Map<K, V>) => K2 | 键生成函数,接收"值—键"对并为每个条目生成新键 |
其中getKeyFromEntry回调的三个入参分别是:
value: V:当前条目的值;key: K:当前条目在原 Map中的键;object: Map<K, V>:原 Map 对象本身,可在需要访问整体信息(如map.size)时使用。
返回值(Returns)
Map<K2, V>:新生成的键映射到原条目值的新 Map。
从类型签名可以看出三个泛型参数的职责:K是原 Map 的键类型,V是值类型,K2是新键的类型。新键类型K2与原键类型K完全解耦,这意味着新键可以是字符串、数字、Symbol甚至任意类型(在 JS 中Map的键不像普通对象那样被限制为字符串或 Symbol),这也正是 Map 版keyBy相比数组版(返回Record)更灵活的地方。
源码级实现解析
keyBy的实现非常精巧,完整代码位于 src/map/keyBy.ts,仅有十几行:
export function keyBy<K, V, K2>( map: Map<K, V>, getKeyFromEntry: (value: V, key: K, object: Map<K, V>) => K2 ): Map<K2, V> { const result = new Map<K2, V>(); for (const [key, value] of map) { const newKey = getKeyFromEntry(value, key, map); result.set(newKey, value); } return result; }其核心逻辑可以用四步概括:
- 新建空 Map:创建
Map<K2, V>类型的result,作为返回容器; - 遍历原 Map:通过
for...of迭代map的[key, value]条目对; - 生成新键:调用
getKeyFromEntry(value, key, map)得到newKey,这里就是文档中"同时使用值和原键"能力的来源——原 Map 的键和整个 Map 都被显式传入回调; - 写入并返回:执行
result.set(newKey, value),遍历结束后返回result。
从实现上可以直接观察出几个重要的行为特征:
- "最后一个值胜出"的语义源自
Map.set的覆盖行为:result.set(newKey, value)对新键重复赋值时,后写入的值天然覆盖先前的值,无需任何额外的冲突判断代码; - 不修改原 Map:整个过程只向新建的
result写入,原map没有被删除或改写任何条目,是纯粹的函数式操作(这一点也有专门的测试用例保障,见下文); - 时间复杂度为 O(n):单次线性遍历,没有嵌套循环或排序操作,性能开销极小;
- 保持插入顺序:由于
Map本身维护插入顺序,且result完全按原遍历顺序写入,新 Map 中唯一键的顺序与原 Map 顺序一致。
该函数从 src/map/index.ts 中对外导出,与countBy、mapKeys、mapValues、findKey等 Map 工具函数一同构成 es-toolkit 的map模块。
与相邻 Map 工具函数的对比
在 es-toolkit 的map模块中,keyBy并非孤军奋战,理解它与兄弟函数的差异有助于选对工具:
keyByvsmapKeys:mapKeys 的源码 同样接收(value, key, object)形式的转换函数,但它只变换键、不改变值的归属逻辑,且要求新键类型与原键类型一致(泛型K不变),返回的仍是同键类型的 Map;而keyBy允许新键类型K2完全独立,且天然具备"同键取最后"的归组能力。keyByvscountBy:countBy 的源码 也按(value, key, object)生成分组键,但返回值是Map<K2, number>,即每个分组键对应的条目计数,而不是条目本身;keyBy保留的是值。二者一个用于"数一数有多少",一个用于"按类存下代表值"。keyBy(Map 版)vskeyBy(数组版):数组版实现在 src/array/keyBy.ts,接收数组并返回Record<K, T>普通对象;Map 版接收Map并返回Map<K2, V>,且键类型不受PropertyKey(字符串 | 数字 | Symbol)限制。
测试覆盖:行为边界的全面验证
keyBy的行为正确性在 src/map/keyBy.spec.ts 中有系统性的验证,测试用例覆盖了文档中承诺的每一项语义,非常适合作为理解函数边界的参考:
- 按值属性映射:验证
keyBy(map, item => item.type)的归组结果; - 同键取最后一个:多个条目生成相同键时,后遍历到的值胜出;
- 回调可访问原键:
(_value, key) => \key_${key}`` 验证第二个参数确实传入原 Map 的键; - 回调可访问原 Map:通过
expect(originalMap).toBe(map)严格断言第三个参数就是原 Map 对象本身,且能读取originalMap.size; - 空 Map 与单条目 Map:空 Map 返回空
Map(),单条目正常返回; - 不修改原 Map:调用前后用
Array.from(map.entries())对比,确认原 Map 条目完全不变; - 数字键:验证值以数字开头作为新键时行为正确;
- Symbol 键:验证
Symbol可以作为新键——这是普通对象Record无法直接等价做到的特性; - 全部条目映射到同一键:
keyBy(map, () => 'same')返回仅含一个条目的 Map,且值取最后一项; - 复杂对象作为值:确认对象引用被原样保留(不进行深拷贝);
- 唯一键保持插入顺序:
Array.from(result.keys())的结果与原顺序一致。
这些用例不仅验证了功能正确性,也从侧面印证了上文的实现推断:纯新建容器、无副作用、线性遍历、顺序保留。
适用前提与注意事项
- 导入路径:请务必从
es-toolkit/map导入keyBy,而非es-toolkit根入口,否则可能与数组版keyBy产生命名冲突或无法解析; - 键冲突策略:
keyBy的"同键后者胜出"是确定性的语义,如果你的业务要求保留第一个匹配项,需要在使用时自行反转数据顺序或改用其他工具; - 返回新 Map:
keyBy不会原地修改原 Map,如果需要同时持有原数据,无需担心副作用; - 对象值保持引用:返回的 Map 中的值直接引用原对象,不会发生深拷贝,修改返回值中的对象会影响原 Map 中的同一对象,这一点与测试中"复杂对象作为值"的断言一致。
总而言之,keyBy是 es-toolkitmap模块中一个"小而美"的工具:它的实现只有十几行,却借助Map自身的特性优雅地完成了键重写、分类归组与索引重建等常见数据重组需求,是处理键值型数据的日常得力助手。
【免费下载链接】es-toolkitA modern JavaScript utility library that's 2-3 times faster and up to 97% smaller, a major upgrade to lodash.项目地址: https://gitcode.com/GitHub_Trending/es/es-toolkit
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考