数组转哈希映射工具函数封装(toMap方法)

前言

在工作中有时候会遇到这样的场景:将一个数组转换为一个对象,对象键为数组元素的某个属性,对象值为数组元素本身等。所以封装了一个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: 'alice@example.com', phone: '123456', address: 'Street 1' },
    { id: 'u2', name: 'Bob', email: 'bob@example.com', 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: 'alice@example.com'  },
  u2: { name: 'Bob', email: 'bob@example.com'  }
}
*/

// 假设当前用户权限为'low'
const lowPermissionMap = userMapFunction(usersDetails, 'low')
/*
{
  u1: { name: 'Alice' },
  u2: { name: 'Bob' }
}
*/
相关推荐
前端大卫6 分钟前
Vue3 + Element-Plus 自定义虚拟表格滚动实现方案【附源码】
前端
却尘21 分钟前
Next.js 请求最佳实践 - vercel 2026一月发布指南
前端·react.js·next.js
ccnocare23 分钟前
浅浅看一下设计模式
前端
Lee川26 分钟前
🎬 从标签到屏幕:揭秘现代网页构建与适配之道
前端·面试
Ticnix1 小时前
ECharts初始化、销毁、resize 适配组件封装(含完整封装代码)
前端·echarts
纯爱掌门人1 小时前
终焉轮回里,藏着 AI 与人类的答案
前端·人工智能·aigc
twl1 小时前
OpenClaw 深度技术解析
前端
崔庆才丨静觅1 小时前
比官方便宜一半以上!Grok API 申请及使用
前端
星光不问赶路人1 小时前
vue3使用jsx语法详解
前端·vue.js
天蓝色的鱼鱼1 小时前
shadcn/ui,给你一个真正可控的UI组件库
前端