前言
在工作中有时候会遇到这样的场景:将一个数组转换为一个对象,对象键为数组元素的某个属性,对象值为数组元素本身等。所以封装了一个toMap方法,将数组转换为哈希映射对象。在此记录一下。
toMap方法 将数组转换为哈希映射对象
typescript
/**
* 将数组转换为哈希映射对象
* @param keyMapper 键生成函数(默认使用 item.id )
* @param valueMapper 值生成函数(默认返回元素本身)
* @param mergeMapper 重复键合并策略(默认新值覆盖旧值)
*
* @example
* const userMap = toMap()([{id:1,name:'Alice'}, {id:2,name:'Bob'}])
* // => { 1: {id:1,name:'Alice'}, 2: {id:2,name:'Bob'} }
*/
export function toMap<T, K extends string | number | symbol = string, V = T, E extends any[] = any[]>(
keyMapper?: (item: T, ...extraArgs: E) => K,
valueMapper?: (item: T, ...extraArgs: E) => V,
mergeMapper?: (prevItem: T, currItem: T, prevValue: V, currValue: V, ...extraArgs: E) => V
): (arr: T[], ...extraArgs: E) => Record<K, V> {
// 默认键生成(要求元素有id属性)
const defaultKeyMapper = (item: any) => item.id as K
// 默认值生成(返回元素本身)
const defaultValueMapper = (item: T) => item as unknown as V
// 默认合并策略(新值覆盖旧值)
const defaultMergeMapper = (_prevItem: T, _currItem: T, _prevValue: V, currValue: V) => currValue
const keyFn = keyMapper || defaultKeyMapper
const valueFn = valueMapper || defaultValueMapper
const mergeFn = mergeMapper || defaultMergeMapper
return (arr: T[], ...extraArgs: E): Record<K, V> => {
const result = {} as Record<K, V>
const itemMap = {} as Record<K, T>
arr.forEach((item) => {
const key = keyFn(item, ...extraArgs)
const value = valueFn(item, ...extraArgs)
if (key in result) {
const prevItem = itemMap[key]
const prevValue = result[key]
result[key] = mergeFn(prevItem, item, prevValue, value, ...extraArgs)
} else {
result[key] = value
}
itemMap[key] = item // 更新引用
})
return result
}
}
1. 基础用法(按 ID 映射)
typescript
import { toMap } from '@/utils/collection'
const users = [
{ id: 'u1', name: 'Alice' },
{ id: 'u2', name: 'Bob' }
]
// 生成 { u1: {id:'u1',name:'Alice'}, u2: {...} }
const userMap = toMap()(users)
2. 自定义键名 + 值转换
typescript
// 使用用户名作为键,只保留年龄
const config = toMap(
(user) => user.name,
(user) => ({ age: user.age })
)
const userData = [
{ name: 'Alice', age: 30 },
{ name: 'Bob', age: 25 }
]
// 结果: { Alice: {age:30}, Bob: {age:25} }
const userAgeMap = config(userData)
3. 重复键合并策略
typescript
const orders = [
{ userId: 'u1', amount: 100 },
{ userId: 'u1', amount: 200 }
]
// 合并相同用户的订单金额
const orderMap = toMap(
(order) => order.userId,
(order) => order.amount,
(prev, curr, prevAmount, currAmount) => prevAmount + currAmount
)(orders)
// 结果: { u1: 300 }
4. 动态参数场景
a. 根据动态前缀生成键
假设我们有一组用户数据,我们想根据不同的环境(如测试环境、生产环境)生成不同的键前缀。
typescript
const users = [
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' }
]
// 定义toMap配置:键使用动态前缀+id,值保持不变
const toMapWithPrefix = toMap(
(user, prefix: string) => `${prefix}_${user.id}`, // 使用动态参数prefix
(user) => user
)
// 调用时传入动态前缀
const testMap = toMapWithPrefix(users, 'TEST')
// 结果: { TEST_1: {id:1, name:'Alice'}, TEST_2: {id:2, name:'Bob'} }
const prodMap = toMapWithPrefix(users, 'PROD')
// 结果: { PROD_1: {id:1, name:'Alice'}, PROD_2: {id:2, name:'Bob'} }
b. 根据当前时间过滤或转换值
假设我们有一个订单数组,我们想根据当前日期(动态传入)来判断订单是否过期,并在映射的值中体现。
typescript
interface Order {
id: string
dueDate: string // 到期日,格式为YYYY-MM-DD
amount: number
}
const orders: Order[] = [
{ id: 'o1', dueDate: '2025-06-01', amount: 100 },
{ id: 'o2', dueDate: '2025-06-10', amount: 200 }
]
// 定义值映射函数:根据传入的当前日期判断订单是否过期
const valueMapper = (order: Order, currentDate: string) => {
const isExpired = order.dueDate < currentDate
return {
...order,
isExpired
}
}
// 创建映射函数
const orderMapFunction = toMap<Order, string, { isExpired: boolean } & Order>((order) => order.id, valueMapper)
// 假设今天是2025-06-05,传入当前日期
const orderMap = orderMapFunction(orders, '2025-06-05')
/*
结果:
{
o1: { id: 'o1', dueDate: '2025-06-01', amount: 100, isExpired: true },
o2: { id: 'o2', dueDate: '2025-06-10', amount: 200, isExpired: false },
}
*/
c. 合并策略依赖外部参数
例如,我们有一个数组代表不同用户在不同地区的销售记录,键由用户ID和地区ID组合而成(动态生成键),在合并时,我们需要根据传入的权重参数对销售额进行加权合并。
typescript
interface SalesRecord {
userId: string
regionId: string
sales: number
}
const records: SalesRecord[] = [
{ userId: 'u1', regionId: 'r1', sales: 100 },
{ userId: 'u1', regionId: 'r1', sales: 200 }, // 同一个用户同一个地区的重复记录
{ userId: 'u2', regionId: 'r2', sales: 150 }
]
// 键映射:组合userId和regionId
const keyMapper = (record: SalesRecord) => `${record.userId}_${record.regionId}`
// 值映射:直接取sales
const valueMapper = (record: SalesRecord) => record.sales
// 合并映射:根据传入的权重(weight)进行加权平均
const mergeMapper = (
prevRecord: SalesRecord,
currentRecord: SalesRecord,
prevValue: number,
currentValue: number,
weight: number // 动态参数:权重,比如0.5表示各取一半
) => {
// 这里简单示例为加权平均:旧值乘以权重,新值乘以(1-权重),然后相加
return prevValue * weight + currentValue * (1 - weight)
}
// 创建映射函数,并传入权重0.6(即旧记录占60%,新记录占40%)
const salesMapFunction = toMap<SalesRecord, string, number, [number]>(keyMapper, valueMapper, mergeMapper)
const salesMap = salesMapFunction(records, 0.6)
/*
对于键 'u1_r1',第一次遇到时记录为100,第二次遇到时合并计算:
第一次:100 -> 存入
第二次:合并:100 * 0.6 + 200 * 0.4 = 60 + 80 = 140
所以结果中 'u1_r1' 的值为140,而其他键正常。
*/
// 如果我们改变权重,比如0.8(更看重旧值)
const salesMap2 = salesMapFunction(records, 0.8) // 结果:100*0.8 + 200*0.2 = 80+40=120
d. 国际化(语言动态切换)
假设我们有一个产品数组,产品信息包含多语言的名称。我们根据传入的语言参数来提取对应语言的名称作为值。
typescript
interface Product {
id: string
name: {
en: string
zh: string
ja: string
}
price: number
}
const products: Product[] = [
{ id: 'p1', name: { en: 'Apple', zh: '苹果', ja: 'りんご' }, price: 10 },
{ id: 'p2', name: { en: 'Banana', zh: '香蕉', ja: 'バナナ' }, price: 5 }
]
// 值映射:根据传入的语言参数获取对应语言的产品名
const valueMapper = (product: Product, lang: keyof Product['name']) => {
return {
name: product.name[lang],
price: product.price
}
}
// 创建映射函数
const productMapFunction = toMap<Product, string, { name: string; price: number }, [keyof Product['name']]>(
(product) => product.id,
valueMapper
)
// 传入语言为中文
const zhProductMap = productMapFunction(products, 'zh')
/*
{
p1: { name: '苹果', price: 10 },
p2: { name: '香蕉', price: 5 }
}
*/
// 传入语言为英文
const enProductMap = productMapFunction(products, 'en')
/*
{
p1: { name: 'Apple', price: 10 },
p2: { name: 'Banana', price: 5 }
}
*/
e. 权限控制下的数据映射
假设我们有一个包含敏感信息的数据数组,我们需要根据当前用户的权限等级来映射值,例如权限高的用户能看到详细信息,权限低的只能看到部分信息。
typescript
interface UserDetail {
id: string
name: string
email: string
phone: string
address: string
}
const usersDetails: UserDetail[] = [
{ id: 'u1', name: 'Alice', email: '[email protected]', phone: '123456', address: 'Street 1' },
{ id: 'u2', name: 'Bob', email: '[email protected]', phone: '654321', address: 'Street 2' }
]
// 根据权限等级过滤显示的信息
const valueMapper = (user: UserDetail, permissionLevel: 'high' | 'medium' | 'low') => {
if (permissionLevel === 'high') {
return user // 全部信息
} else if (permissionLevel === 'medium') {
return { name: user.name, email: user.email } // 部分信息
} else {
return { name: user.name } // 仅名字
}
}
const userMapFunction = toMap<UserDetail, string, Partial<UserDetail>, ['high' | 'medium' | 'low']>(
(user) => user.id,
valueMapper
)
// 假设当前用户权限为'medium'
const mediumPermissionMap = userMapFunction(usersDetails, 'medium')
/*
{
u1: { name: 'Alice', email: '[email protected]' },
u2: { name: 'Bob', email: '[email protected]' }
}
*/
// 假设当前用户权限为'low'
const lowPermissionMap = userMapFunction(usersDetails, 'low')
/*
{
u1: { name: 'Alice' },
u2: { name: 'Bob' }
}
*/