数组转哈希映射工具函数封装(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: '[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' }
}
*/
相关推荐
去旅行、在路上24 分钟前
chrome使用手机调试触屏web
前端·chrome
Aphasia3111 小时前
模式验证库——zod
前端·react.js
lexiangqicheng1 小时前
es6+和css3新增的特性有哪些
前端·es6·css3
拉不动的猪2 小时前
都25年啦,还有谁分不清双向绑定原理,响应式原理、v-model实现原理
前端·javascript·vue.js
烛阴2 小时前
Python枚举类Enum超详细入门与进阶全攻略
前端·python
孟孟~3 小时前
npm run dev 报错:Error: error:0308010C:digital envelope routines::unsupported
前端·npm·node.js
孟孟~3 小时前
npm install 报错:npm error: ...node_modules\deasync npm error command failed
前端·npm·node.js
狂炫一碗大米饭3 小时前
一文打通TypeScript 泛型
前端·javascript·typescript
wh_xia_jun3 小时前
在 Spring Boot 中使用 JSP
java·前端·spring boot
二十雨辰3 小时前
[HTML5]快速掌握canvas
前端·html