
目标:从 Lodash 的设计与实现中提炼函数封装技巧,形成可复用的模式与一套可运行的简化工具集(附 TypeScript 版本)。
iteratee-first, data-last 便于函数组合(iteratee, data, options)null/undefined、空集合、路径字符串等统一处理curry/partial 降低调用成本flow/compose 让小函数可流水线化memoize/debounce/throttle 控制代价与频率export function curry<F extends (...args: any[]) => any>(fn: F) {
return function curried(this: any, ...args: any[]): any {
if (args.length >= fn.length) return fn.apply(this, args)
return (...rest: any[]) => curried.apply(this, args.concat(rest))
}
}
export function partial<F extends (...args: any[]) => any>(fn: F, ...preset: any[]) {
return (...rest: any[]) => fn(...preset, ...rest)
}export function once<F extends (...args: any[]) => any>(fn: F) {
let called = false
let result: any
return (...args: Parameters<F>): ReturnType<F> => {
if (!called) { called = true; result = fn(...args) }
return result
}
}
export function memoize<F extends (...args: any[]) => any>(fn: F, resolver?: (...args: Parameters<F>) => string) {
const cache = new Map<string, any>()
return (...args: Parameters<F>): ReturnType<F> => {
const key = resolver ? resolver(...args) : JSON.stringify(args)
if (cache.has(key)) return cache.get(key)
const val = fn(...args)
cache.set(key, val)
return val
}
}export function debounce<F extends (...args: any[]) => any>(fn: F, wait = 0) {
let timer: any = null
return (...args: Parameters<F>) => {
if (timer) clearTimeout(timer)
timer = setTimeout(() => fn(...args), wait)
}
}
export function throttle<F extends (...args: any[]) => any>(fn: F, wait = 0) {
let last = 0
let trailingArgs: any[] | null = null
let timer: any = null
return (...args: Parameters<F>) => {
const now = Date.now()
if (now - last >= wait) {
last = now
fn(...args)
} else {
trailingArgs = args as any[]
if (!timer) {
const remain = wait - (now - last)
timer = setTimeout(() => { timer = null; last = Date.now(); if (trailingArgs) fn(...trailingArgs); trailingArgs = null }, remain)
}
}
}
}export function flow<T>(...fns: Array<(x: T) => T>) {
return (input: T) => fns.reduce((acc, f) => f(acc), input)
}
export function compose<T>(...fns: Array<(x: T) => T>) {
return (input: T) => fns.reduceRight((acc, f) => f(acc), input)
}type Path = string | Array<string | number>
function toPath(p: Path): Array<string | number> {
if (Array.isArray(p)) return p
return p.split('.').map(seg => seg.match(/^\d+$/) ? Number(seg) : seg)
}
export function get(obj: any, path: Path, defaultValue?: any) {
const ps = toPath(path)
let cur = obj
for (let i = 0; i < ps.length; i++) {
if (cur == null) return defaultValue
cur = cur[ps[i] as any]
}
return cur === undefined ? defaultValue : cur
}
export function set(obj: any, path: Path, value: any) {
const ps = toPath(path)
let cur = obj
for (let i = 0; i < ps.length - 1; i++) {
const key = ps[i]
if (cur[key as any] == null) cur[key as any] = typeof ps[i + 1] === 'number' ? [] : {}
cur = cur[key as any]
}
cur[ps[ps.length - 1] as any] = value
return obj
}export function pickBy<T extends Record<string, any>>(obj: T, predicate: (v: any, k: string) => boolean) {
const out: Record<string, any> = {}
for (const k in obj) if (predicate(obj[k], k)) out[k] = obj[k]
return out as T
}
export function uniqBy<T>(arr: T[], iteratee: (x: T) => any) {
const seen = new Set<string>()
const out: T[] = []
for (let i = 0; i < arr.length; i++) {
const key = JSON.stringify(iteratee(arr[i]))
if (!seen.has(key)) { seen.add(key); out.push(arr[i]) }
}
return out
}export function flattenDeep(arr: any[]): any[] {
const out: any[] = []
const stack = [...arr]
while (stack.length) {
const v = stack.pop()
if (Array.isArray(v)) for (let i = v.length - 1; i >= 0; i--) stack.push(v[i])
else out.push(v)
}
return out.reverse()
}
export function chunk<T>(arr: T[], size = 1) {
const out: T[][] = []
for (let i = 0; i < arr.length; i += size) out.push(arr.slice(i, i + size))
return out
}export function groupBy<T>(arr: T[], iteratee: (x: T) => string | number) {
const out: Record<string, T[]> = {}
for (let i = 0; i < arr.length; i++) {
const k = String(iteratee(arr[i]))
if (!out[k]) out[k] = []
out[k].push(arr[i])
}
return out
}
export function mapValues<T extends Record<string, any>, R>(obj: T, iteratee: (v: any, k: string) => R) {
const out: Record<string, R> = {}
for (const k in obj) out[k] = iteratee(obj[k], k)
return out
}const add = (a: number, b: number, c: number) => a + b + c
const addCurried = curry(add)
const sum = addCurried(1)(2)(3)
const fn = once(() => Math.random())
const r1 = fn(); const r2 = fn()
const heavy = memoize((x: number) => x * x)
const h1 = heavy(9); const h2 = heavy(9)
const d = debounce((v: number) => v * 2, 100)
const t = throttle((v: number) => v * 2, 100)
const pipeline = flow<number>(x => x + 1, x => x * 2)
const p = pipeline(10)
const obj = { a: { b: [1, 2, { c: 3 }] } }
const v = get(obj, 'a.b.2.c')
set(obj, ['a', 'b', 0], 99)
const users = [{ id: 1, role: 'a' }, { id: 2, role: 'a' }, { id: 1, role: 'b' }]
const unique = uniqBy(users, x => x.id)
const grouped = groupBy(users, x => x.role)any 传播Parameters/ReturnType 推断,提升组合体验Path 同时支持 string/array,以简化外部调用负担null/undefined 输入早返回或使用默认值memoize 的 key 选择需可控,复杂对象建议自定义 resolverget/set 在遇到数字索引与对象键时动态创建容器,避免 TypeErrormemoize/throttle/debounce 控制调用开销flow/composepartition 将数组按条件分为两组get 支持安全可选链与默认数组创建策略memoize 加入 maxSize 的 LRU 缓存策略curry/partial/memoize/flow/get 等核心模式,即可快速搭建稳定的工具集export function prop<T, K extends keyof T>(key: K) {
return (x: T) => x[key]
}
export function matches<T>(spec: Partial<T>) {
return (x: T) => {
for (const k in spec) if ((x as any)[k] !== (spec as any)[k]) return false
return true
}
}
export function pluck<T, K extends keyof T>(arr: T[], key: K): Array<T[K]> {
return arr.map(x => x[key])
}
export function compact<T>(arr: Array<T | null | undefined | false | 0 | ''>) {
return arr.filter(Boolean) as T[]
}type Op<T> = (input: T[]) => T[]
class Seq<T> {
private ops: Op<T>[] = []
constructor(private source: T[]) {}
map(fn: (x: T) => T) { this.ops.push(arr => arr.map(fn)); return this }
filter(fn: (x: T) => boolean) { this.ops.push(arr => arr.filter(fn)); return this }
take(n: number) { this.ops.push(arr => arr.slice(0, n)); return this }
value() { return this.ops.reduce((a, op) => op(a), this.source) }
}
export function seq<T>(arr: T[]) { return new Seq(arr) }export function deepClone<T>(x: T): T {
const sc = (globalThis as any).structuredClone
if (typeof sc === 'function') return sc(x)
return JSON.parse(JSON.stringify(x))
}
export function isEqual(a: any, b: any): boolean {
if (a === b) return true
if (a && b && typeof a === 'object' && typeof b === 'object') {
if (Array.isArray(a) && Array.isArray(b)) {
if (a.length !== b.length) return false
for (let i = 0; i < a.length; i++) if (!isEqual(a[i], b[i])) return false
return true
}
const ak = Object.keys(a)
const bk = Object.keys(b)
if (ak.length !== bk.length) return false
for (let i = 0; i < ak.length; i++) {
const k = ak[i]
if (!isEqual(a[k], b[k])) return false
}
return true
}
return false
}
export function merge(target: any, ...sources: any[]) {
for (let i = 0; i < sources.length; i++) {
const src = sources[i]
if (!src || typeof src !== 'object') continue
for (const k of Object.keys(src)) {
const sv = src[k]
const tv = target[k]
if (sv && typeof sv === 'object' && !Array.isArray(sv)) {
target[k] = merge(tv && typeof tv === 'object' && !Array.isArray(tv) ? tv : {}, sv)
} else {
target[k] = Array.isArray(sv) ? sv.slice() : sv
}
}
}
return target
}
export function defaults<T extends Record<string, any>>(obj: T, def: Partial<T>): T {
const out: any = { ...obj }
for (const k in def) if (out[k] === undefined) out[k] = (def as any)[k]
return out
}export function tryCatch<F extends (...args: any[]) => any>(fn: F, fallback: any) {
return (...args: Parameters<F>): ReturnType<F> => {
try { return fn(...args) } catch { return typeof fallback === 'function' ? (fallback as any)(...args) : fallback }
}
}type RawUser = { id: number; name: string; role?: string | null }
type VMUser = { id: number; name: string; role: string }
function normalizeUsers(raw: RawUser[]): VMUser[] {
const filled = raw.map(u => defaults(u, { role: 'guest' }))
const unique = uniqBy(filled, prop<RawUser, 'id'>('id'))
const sorted = unique.slice().sort((a, b) => String(a.role).localeCompare(String(b.role)))
return sorted.map(u => ({ id: u.id, name: u.name, role: String(u.role) }))
}
const result = seq(normalizeUsers([
{ id: 1, name: 'a', role: null },
{ id: 1, name: 'a' },
{ id: 2, name: 'b', role: 'admin' }
])).filter(x => x.role !== 'guest').take(2).value().value() 前不创建临时数组memoize/throttle/debounce,对重计算与高频交互限流merge 增加数组合并策略,如去重或拼接differenceBy/intersectionBy 基于 iteratee 的集合运算tap 与 noop 用于流水线调试与占位