es-toolkit 的 Map 版 keyBy:用键生成函数高效重组 Map 数据结构的实战指南
2026/9/17 5:38:38 网站建设 项目流程

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'

这里有两个要点值得强调:

  1. 新键来自值的内容:原 Map 中'x''y'两个条目的值都属于fruit类型,因此它们被归并到同一个新键'fruit'下,Map的大小从 3 缩小为 2。
  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' } // }

可以看到,user2user3都属于'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)

参数类型说明
mapMap<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; }

其核心逻辑可以用四步概括:

  1. 新建空 Map:创建Map<K2, V>类型的result,作为返回容器;
  2. 遍历原 Map:通过for...of迭代map[key, value]条目对;
  3. 生成新键:调用getKeyFromEntry(value, key, map)得到newKey,这里就是文档中"同时使用值和原键"能力的来源——原 Map 的键和整个 Map 都被显式传入回调;
  4. 写入并返回:执行result.set(newKey, value),遍历结束后返回result

从实现上可以直接观察出几个重要的行为特征:

  • "最后一个值胜出"的语义源自Map.set的覆盖行为result.set(newKey, value)对新键重复赋值时,后写入的值天然覆盖先前的值,无需任何额外的冲突判断代码;
  • 不修改原 Map:整个过程只向新建的result写入,原map没有被删除或改写任何条目,是纯粹的函数式操作(这一点也有专门的测试用例保障,见下文);
  • 时间复杂度为 O(n):单次线性遍历,没有嵌套循环或排序操作,性能开销极小;
  • 保持插入顺序:由于Map本身维护插入顺序,且result完全按原遍历顺序写入,新 Map 中唯一键的顺序与原 Map 顺序一致。

该函数从 src/map/index.ts 中对外导出,与countBymapKeysmapValuesfindKey等 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的"同键后者胜出"是确定性的语义,如果你的业务要求保留第一个匹配项,需要在使用时自行反转数据顺序或改用其他工具;
  • 返回新 MapkeyBy不会原地修改原 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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询