从第三方库中偷师:学习 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 选择需可控,复杂对象建议自定义resolverget/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 的集合运算 - 封装
tap与noop用于流水线调试与占位