从第三方库中偷师:学习 Lodash 的函数封装技巧

从第三方库中偷师:学习 Lodash 的函数封装技巧

目标:从 Lodash 的设计与实现中提炼函数封装技巧,形成可复用的模式与一套可运行的简化工具集(附 TypeScript 版本)。

思维模型概览

  • 数据最后、迭代器优先:iteratee-first, data-last 便于函数组合
  • 小函数拼装:单一职责的小工具,通过组合产生复杂行为
  • 惰性与短路:在链式或迭代中尽早停止,节省开销
  • 输入归一化:多形态参数统一为内部标准格式,提升健壮性
  • 无副作用:默认不修改外部数据,返回新值,利于推理与测试

封装的通用技巧

  • 语义清晰的函数签名:固定参数顺序,(iteratee, data, options)
  • 参数归一化与守卫:对 null/undefined、空集合、路径字符串等统一处理
  • 支持占位与偏函数:通过 curry/partial 降低调用成本
  • 可组合性:flow/compose 让小函数可流水线化
  • 性能与缓存:memoize/debounce/throttle 控制代价与频率

简化实现(可运行 TypeScript)

curry 与 partial

ts 复制代码
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)
}

once 与 memoize

ts 复制代码
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
  }
}

debounce 与 throttle

ts 复制代码
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)
      }
    }
  }
}

flow 与 compose

ts 复制代码
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)
}

路径工具:get/set

ts 复制代码
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
}

pickBy 与 uniqBy

ts 复制代码
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
}

flattenDeep 与 chunk

ts 复制代码
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
}

groupBy 与 mapValues

ts 复制代码
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
}

用法示例

ts 复制代码
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 选择需可控,复杂对象建议自定义 resolver
  • get/set 在遇到数字索引与对象键时动态创建容器,避免 TypeError

性能实践

  • 迭代优先使用 for 索引循环,避免多余闭包与中间数组
  • 使用短路与惰性策略,减少不必要计算
  • 对高频函数添加 memoize/throttle/debounce 控制调用开销

设计准则清单

  • 单一职责:每个函数只做一件事
  • 可组合性:输入输出结构对齐,便于 flow/compose
  • 不可变:尽量返回新数据,避免隐藏副作用
  • 输入归一化:灵活外部接口,稳定内部实现

练习

  • 实现 partition 将数组按条件分为两组
  • 扩展 get 支持安全可选链与默认数组创建策略
  • memoize 加入 maxSize 的 LRU 缓存策略

总结

  • Lodash 的价值不止于"有多少函数",更在于封装模式与健壮性
  • 掌握 curry/partial/memoize/flow/get 等核心模式,即可快速搭建稳定的工具集
  • 在实际项目中以小函数组合为先,性能与边界控制贯穿始终

进阶:iteratee 简写与谓词封装

ts 复制代码
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[]
}

进阶:轻量链式序列(延迟至 value)

ts 复制代码
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) }

进阶:深拷贝、深相等与合并

ts 复制代码
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
}

进阶:容错包装与安全执行

ts 复制代码
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 }
  }
}

实战片段:规范接口数据并输出视图模型

ts 复制代码
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()

边界强化清单(进阶)

  • iteratee 接受字符串、对象、函数三类形式,内部统一为函数
  • 路径工具支持数字索引与对象键,避免越界时抛错
  • 深拷贝在不可序列化数据上转为浅拷贝或显式失败
  • 缓存策略限定尺寸与过期,避免内存膨胀

性能与微优化

  • 优先 for 索引循环与复用中间变量,减少闭包分配
  • 对链式处理采用延迟计算,在 .value() 前不创建临时数组
  • 高频函数配合 memoize/throttle/debounce,对重计算与高频交互限流

小练习(扩展)

  • merge 增加数组合并策略,如去重或拼接
  • 实现 differenceBy/intersectionBy 基于 iteratee 的集合运算
  • 封装 tapnoop 用于流水线调试与占位
相关推荐
lingggggaaaa4 小时前
免杀对抗——C2远控篇&C&C++&DLL注入&过内存核晶&镂空新增&白加黑链&签名程序劫持
c语言·c++·学习·安全·网络安全·免杀对抗
陈天伟教授5 小时前
基于学习的人工智能(5)机器学习基本框架
人工智能·学习·机器学习
我先去打把游戏先5 小时前
ESP32学习笔记(基于IDF):基于OneNet的ESP32的OTA功能
笔记·物联网·学习·云计算·iphone·aws
初願致夕霞5 小时前
学习笔记——基础hash思想及其简单C++实现
笔记·学习·哈希算法
小女孩真可爱5 小时前
大模型学习记录(五)-------调用大模型API接口
pytorch·深度学习·学习
hd51cc5 小时前
C++ 学习笔记 名称
笔记·学习
cmcm!7 小时前
学习笔记1
数据库·笔记·学习
Hcoco_me8 小时前
YOLO目标检测学习路线图
学习·yolo·目标检测
WXG101110 小时前
【Flask】前后端交互示例
笔记·学习