es-toolkit 的 omitBy 兼容实现解析:按谓词动态过滤对象属性的完整指南
【免费下载链接】es-toolkitA modern JavaScript utility library that's 2-3 times faster and up to 97% smaller, a major upgrade to lodash.项目地址: https://gitcode.com/GitHub_Trending/es/es-toolkit
omitBy是 es-toolkit 提供的对象属性过滤工具:对对象的每个属性执行谓词函数,凡是谓词返回true的属性都会被剔除,最终返回一个只包含"不满足条件"属性的新对象。本文以 compat 兼容版文档 为核心骨架,结合 src/compat/object/omitBy.ts 源码与 src/compat/object/omitBy.spec.ts 测试用例,完整讲解其用法、参数语义、边界行为、底层实现原理,并与更快的现代版es-toolkit/object进行对比,帮助你按需选择正确的 API。
注意:本文讲解的
omitBy来自es-toolkit/compat入口,旨在提供与 Lodash 行为高度一致的兼容实现;如果你不需要 Lodash 兼容语义,官方强烈推荐使用更快、更现代的es-toolkit/object版本(见文末对比小节)。
一、omitBy 解决什么问题
在业务开发中,经常需要"按条件剔除属性":比如删除对象中所有字符串类型的值、剔除所有空字符串、按 key 前缀过滤配置项等。omitBy正是为此设计的——它接收一个源对象和一个谓词函数(predicate),遍历对象每个属性,将谓词返回true的属性从结果中剔除:
const result = omitBy(obj, predicate);与pickBy(保留满足条件的属性)相反,omitBy保留的是不满足条件的属性,二者互为镜像。它在需要根据运行时条件动态过滤对象时非常有用,无需预先枚举要删除的 key。
二、安装与导入
本项目是 monorepo 形态的开源仓库,使用 Yarn 管理依赖(见仓库根目录 package.json 与yarn.lock)。在业务项目中安装 es-toolkit 后,按入口导入:
// 兼容版(Lodash 互換,语义与 Lodash 对齐) import { omitBy } from 'es-toolkit/compat'; // 现代版(更快的原生实现) import { omitBy } from 'es-toolkit/object';两条入口都导出了同名omitBy,但行为存在差异(见第五节与第九节),请根据实际需求选择。
三、基础用法:五个典型场景
原文档给出了 5 个覆盖不同谓词形态的示例,全部继承如下:
3.1 删除特定类型的值
import { omitBy } from 'es-toolkit/compat'; const data = { a: 1, b: 'remove', c: 3, d: 'keep' }; const numbers = omitBy(data, value => typeof value === 'string'); // 结果: { a: 1, c: 3 }3.2 按真值条件删除(剔除所有假值)
const user = { id: 1, name: 'John', age: 0, active: false, email: '' }; const validData = omitBy(user, value => !value); // 结果: { id: 1, name: 'John' }(age: 0、active: false、email: '' 均为假值,被剔除)3.3 按 key 名过滤
谓词除了接收 value,还会接收属性名 key,因此可以基于 key 做过滤:
const settings = { userSetting: true, adminSetting: false, debugMode: true }; const userOnly = omitBy(settings, (value, key) => key.startsWith('admin')); // 结果: { userSetting: true, debugMode: true }3.4 混合对象中剔除数值属性
const mixed = { str: 'hello', num1: 42, bool: true, num2: 0, obj: {} }; const noNumbers = omitBy(mixed, value => typeof value === 'number'); // 结果: { str: 'hello', bool: true, obj: {} }3.5 对数组使用
数组是特殊的对象(索引为 key),omitBy同样适用,结果会以"索引字符串为 key"的对象形式返回:
const arr = [1, 2, 3, 4, 5]; const filtered = omitBy(arr, value => value % 2 === 0); // 结果: { '0': 1, '2': 3, '4': 5 }(值为奇数、索引为 '0'/'2'/'4' 的属性被保留)3.6 同时使用 value、key 与源对象
虽然类型签名只声明了两个参数,但兼容版实现实际会以三个参数调用谓词(见源码 L95:predicate(value, key, object)),因此你可以在谓词中拿到原始源对象做统计或对照:
const scores = { math: 90, science: 75, english: 85, art: 60 }; const passingGrades = omitBy(scores, (value, key, obj) => { console.log(`${key}: ${value} (平均: ${Object.values(obj).reduce((a, b) => a + b) / Object.keys(obj).length})`); return value < 80; }); // 结果: { math: 90, english: 85 }四、边界处理:null 与 undefined
omitBy对null或undefined源对象采取宽容策略——将其视为空对象处理,返回{}而不是抛错:
import { omitBy } from 'es-toolkit/compat'; omitBy(null, () => true); // {} omitBy(undefined, () => true); // {}这一点在源码中体现得非常直接:实现的第一行就是空值短路(src/compat/object/omitBy.ts):
if (object == null) { return {}; }注意这里用的是宽松相等== null,因此同时覆盖了null和undefined两种情况。对应的测试用例位于 omitBy.spec.ts:当源对象为null时,无论谓词为何,结果均为{}。
五、参数与返回值详解
参数
| 参数 | 类型 | 说明 |
|---|---|---|
object | Record<string, T> \| Record<number, T> \| object \| null \| undefined | 待过滤的源对象,支持字符串键对象、数值键对象、数组、普通对象,以及null/undefined |
predicate | ValueKeyIteratee<T[keyof T]> \| ValueKeyIteratee<T>(可选) | 对每个属性执行的谓词函数,返回true的属性将被剔除;默认值为identity函数(即原样返回输入值) |
predicate是可选的。若省略,会退化为默认的identity谓词——此时等价于"保留所有假值属性、剔除所有真值属性"。这一默认值在实现中通过createIteratee(shouldOmit ?? identity)生效(src/compat/object/omitBy.ts),identity定义于 src/compat/function/identity.ts。
测试中专门覆盖了该场景:omitBy({ a: 1, b: 'omit', c: 3 }, null)返回{}(omitBy.spec.ts)——因为null谓词被?? identity兜底为恒等函数后,三个属性的值均为真值,于是全部被剔除。
返回值
Record<string, S> | Record<number, S> | Partial<T>:由所有"谓词返回 false(不满足条件)"的属性构成的新对象。函数不会修改原对象,始终返回全新的对象。
谓词的类型:ValueKeyIteratee
兼容版的谓词类型来自ValueKeyIteratee<T>(定义于 src/compat/_internal/ValueKeyIteratee.ts):
export type ValueKeyIteratee<T> = ((value: T, key: string) => unknown) | IterateeShorthand<T>;它可以是:
- 函数:
(value, key) => unknown,最常用; - IterateeShorthand 简写:
PropertyKey | [PropertyKey, any] | PartialShallow<T>,即属性名、[属性名, 值]二元组或部分对象。
这得益于实现内部统一通过createIteratee(即 src/compat/util/iteratee.ts 中的iteratee转换器)把任意形态的谓词归一为函数,转换规则如下:
- 函数:原样返回;
- 属性名 / Symbol / 数值 key:转换为
property(key),即"取出该属性的值"; [属性名, 值]二元组:转换为matchesProperty,即"判断该属性是否等于指定值";- 普通对象:转换为
matches,即"判断源对象是否匹配该部分对象"; null/undefined:返回identity。
因此,omitBy的谓词既可以是回调函数,也可以直接传入'active'、['type', 'admin']或{ status: 'disabled' }这类 Lodash 风格简写——这正是兼容版与 Lodash 语义对齐的体现。
六、源码实现深度解析:兼容版做了什么
兼容版omitBy的完整实现位于 src/compat/object/omitBy.ts,核心逻辑只有十几行:
export function omitBy<T, S extends T>( object: Record<string, T> | Record<number, T> | object | null | undefined, shouldOmit?: ValueKeyIteratee<T[keyof T]> | ValueKeyIteratee<T> ): Record<string, S> | Record<number, S> | Partial<T> { if (object == null) { return {}; } const result: Partial<T> = {}; const predicate = createIteratee(shouldOmit ?? identity); const keys = [...keysIn(object), ...getSymbolsIn(object)] as Array<keyof T>; for (let i = 0; i < keys.length; i++) { const key = (isSymbol(keys[i]) ? keys[i] : keys[i].toString()) as keyof T; const value = object[key as keyof typeof object]; if (!predicate(value, key, object)) { result[key] = value; } } return result; }关键点有三:
6.1 键的收集:keysIn + getSymbolsIn
与简单实现只用Object.keys不同,兼容版拼接了两个来源:
keysIn(object)(src/compat/object/keysIn.ts):返回自身 + 原型链上所有可枚举字符串键(含继承属性)。它对非对象值会先装箱(Object(object)),对数组/类数组对象按索引展开,对原型对象会剔除constructor。getSymbolsIn(object)(src/compat/_internal/getSymbolsIn.ts):沿原型链逐层收集所有 Symbol 键(while (object) { ...; object = Object.getPrototypeOf(object); })。
正因为如此,兼容版omitBy能够处理继承属性和Symbol 键属性,这是它与现代版最本质的实现差异(现代版只用Object.keys,见 src/object/omitBy.ts)。
6.2 键的归一化与 -0 符号保留
遍历时,非 Symbol 键统一调用.toString()转为字符串,Symbol 键保持原样(isSymbol判定来自 src/compat/predicate/isSymbol.ts)。测试用例 omitBy.spec.ts 验证了一个微妙行为:-0与0作为键时符号会被保留——{ '-0': 'a', 0: 'b' }经过omitBy过滤后,-0键与0键不会互相串扰。
6.3 保留条件:!predicate(...)
循环体内只有一行过滤逻辑:if (!predicate(value, key, object)) { result[key] = value; }。谓词返回假值时属性被保留,返回真值时被剔除。注意谓词以(value, key, object)三个实参被调用——第三个参数object正是源对象,这解释了 3.6 节示例中能在谓词内访问obj的原因。
七、测试用例揭示的兼容语义
src/compat/object/omitBy.spec.ts 是理解兼容版行为边界的最好教材,除前文已述的用例(null 源对象、null 谓词、-0/0符号)外,还覆盖了:
- 继承属性被纳入过滤(L28-L35):
Foo.prototype = object后,new Foo()的实例属性连同原型上的属性一起参与谓词过滤,结果与直接过滤源对象一致; - Symbol 键参与过滤且不可枚举 Symbol 不进入结果(L47-L109):自身 Symbol、原型链上的可枚举 Symbol 都会被遍历并参与谓词判断;而通过
Object.defineProperty定义为enumerable: false的 Symbol 不会出现在结果中(symbol3 in actual为false); - 数组对象按索引过滤(L111-L114):
omitBy([1, 2, 3], ...)返回{ 1: 2 },与文档 3.5 节的示例行为一致; - 带数字
length属性的普通对象不会被误判为类数组(L129-L146):{ level: 'error', message: 'hi', length: 104, empty: undefined }配合isNil过滤时,length: 104作为普通属性被保留,同时 Symbol 键属性也完好保留。
这些行为共同保证了与 Lodash 的omitBy语义高度一致,这正是es-toolkit/compat入口存在的意义。
八、性能提示:为什么文档推荐现代版
原文档开头给出了明确的警告:请优先使用 es-toolkit 的现代版omitBy(docs/reference/object/omitBy.md),理由是兼容版相对较慢,慢的来源正是本文分析的三个环节:
- 类数组对象的检查:
keysIn内部需要先判断isArrayLike、isPrototype、isBuffer、isTypedArray等(src/compat/object/keysIn.ts),存在额外分支开销; - iteratee 转换:谓词要经过
createIteratee的简写归一化(函数/属性名/二元组/部分对象的分派),比直接调用回调函数多一层间接; - 键的转换过程:遍历时需要对每个 key 执行
isSymbol判断与.toString()转换,还要额外收集原型链与 Symbol 键。
而现代版实现(src/object/omitBy.ts)只做最朴素的事:Object.keys(obj)取自身可枚举字符串键 → 循环调用shouldOmit(value, key)→ 收集结果,无继承属性、无 Symbol、无 iteratee 简写、无类型转换。如果不需要 Lodash 兼容语义(不依赖继承属性、Symbol 键、-0符号或简写谓词),应使用es-toolkit/object的版本以获得更好的性能;只有迁移 Lodash 代码、需要行为完全一致时才使用es-toolkit/compat。
九、快速选型总结
| 维度 | es-toolkit/compat的 omitBy | es-toolkit/object的 omitBy |
|---|---|---|
| 导入入口 | import { omitBy } from 'es-toolkit/compat' | import { omitBy } from 'es-toolkit/object' |
| 遍历范围 | 自身 + 原型链可枚举键 + Symbol 键 | 仅自身可枚举字符串键 |
| 谓词形态 | 函数 / 属性名 /[key, value]/ 部分对象(iteratee 简写) | 仅函数 |
| 谓词参数 | (value, key, object) | (value, key) |
null/undefined源 | 返回{} | ——(类型上要求对象) |
| 默认谓词 | identity | 必须显式传入 |
| 性能 | 相对较慢(类数组检查 + iteratee 转换 + 键转换) | 更快(最小化实现) |
| 适用场景 | 从 Lodash 迁移、需要完全兼容语义 | 新项目、追求性能 |
总之:omitBy是"反向过滤"的对象工具,谓词返回true即剔除。默认场景推荐现代版;需要 Lodash 级兼容行为时,理解keysIn+getSymbolsIn+createIteratee这套兼容管线,你就能准确预测它在继承属性、Symbol、-0、length属性等边界输入下的行为。
【免费下载链接】es-toolkitA modern JavaScript utility library that's 2-3 times faster and up to 97% smaller, a major upgrade to lodash.项目地址: https://gitcode.com/GitHub_Trending/es/es-toolkit
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考