es-toolkit/compat 的 keys 函数:兼容 Lodash 的自有可枚举属性名获取指南
【免费下载链接】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
本文围绕 es-toolkit 兼容层(es-toolkit/compat)中的keys函数展开,它用于返回对象自身可枚举属性名的字符串数组,与 Lodash 的_.keys行为保持一致。文中将完整覆盖其基本用法、继承属性过滤、类数组对象特殊处理、空值安全语义,并结合仓库源码与测试用例深入剖析其底层实现原理,帮助你在迁移 Lodash 时准确使用该 API,并理解它与原生Object.keys()的性能差异。
一、函数定位:兼容 Lodash 的keys,但优先推荐Object.keys
keys是 es-toolkit 兼容层提供的 Lodash 兼容函数,官方文档在介绍它时首先给出了一条明确的警告:
这个
keys函数由于包含对类数组对象、原型对象等的复杂处理逻辑,运行速度较慢。请优先使用更快、更现代的Object.keys()。
也就是说,keys的存在意义是帮助从 Lodash 平滑迁移的兼容 API,而不是日常开发的首选工具。如果你没有 Lodash 迁移负担,直接用原生Object.keys()即可获得更好的性能;如果你的代码库中大量使用了_.keys(object),则可以将es-toolkit/compat的keys作为直接替换品,两者行为基本一致。
该函数定义于 src/compat/object/keys.ts,并通过 compat 入口 统一导出:
export { keys } from './object/keys.ts';二、安装与导入
keys属于 es-toolkit 的兼容层模块,需要从es-toolkit/compat子路径导入:
import { keys } from 'es-toolkit/compat';注意:es-toolkit/compat与默认的es-toolkit入口是两个不同的包路径。默认入口面向现代 API,而compat入口专门用于兼容 Lodash 的调用方式,keys只能从compat获取。本文后续所有示例均默认使用上述导入语句。
三、基本用法:普通对象、数组与字符串
keys(object)返回对象自身的可枚举属性名数组。最简单的用法是传入一个普通对象:
import { keys } from 'es-toolkit/compat'; // 普通对象的键 const object = { a: 1, b: 2, c: 3 }; keys(object); // => ['a', 'b', 'c'] // 数组的下标 const array = [1, 2, 3]; keys(array); // => ['0', '1', '2'] // 字符串的下标 keys('hello'); // => ['0', '1', '2', '3', '4']几个值得注意的细节:
- 对数组而言,返回的是字符串形式的索引(
'0'、'1'、'2'),这与Object.keys的行为一致; - 对字符串,会先将其视为类数组对象,返回每个字符的索引;
- 返回结果只包含自身属性,不包含继承属性,这一点在下一节展开。
参数与返回值
| 项目 | 说明 |
|---|---|
参数object | any类型,要获取键的对象;可为任意值,包括数组、字符串、函数、null、undefined |
| 返回值 | string[],对象自身可枚举属性名的数组 |
四、排除继承属性:函数与构造函数场景
keys只返回自身的可枚举属性,从函数或构造函数原型链上继承下来的属性会被排除。官方文档给出了经典的构造函数示例:
import { keys } from 'es-toolkit/compat'; function Foo() { this.a = 1; this.b = 2; } Foo.prototype.c = 3; keys(new Foo()); // => ['a', 'b']('c' 是原型上的属性,因此被排除)这一行为与测试用例 src/compat/object/keys.spec.ts 完全一致:测试中构造Foo.prototype.b = 2,最终断言keys(new Foo())只返回['a']。
五、类数组对象的特殊处理
与原生Object.keys不同,keys对类数组对象有专门的逻辑分支,包括 TypedArray、arguments对象、字符串对象等:
import { keys } from 'es-toolkit/compat'; // TypedArray const typedArray = new Uint8Array([1, 2, 3]); keys(typedArray); // => ['0', '1', '2'] // arguments 对象 function example() { return keys(arguments); } example('a', 'b', 'c'); // => ['0', '1', '2']从源码看(src/compat/object/keys.ts),arrayLikeKeys辅助函数会:
- 用
times(object.length, ...)生成['0', '1', ..., 'length-1']索引序列,并通过Set去重; - 对 Buffer 特殊处理:过滤掉
offset和parent两个非索引属性(兼容 Node.js 0.10 时代 Buffer 的行为); - 对 TypedArray 特殊处理:过滤掉
buffer、byteLength、byteOffset三个非索引属性(兼容 PhantomJS 2 的行为); - 对数组直接返回「索引 + 剩余自身属性」;对非数组类数组对象,用
Object.hasOwn再次校验索引是否真实存在。
上述过滤逻辑在测试 src/compat/object/keys.spec.ts 中有明确验证:
// Buffer 不应包含 offset 或 parent 键 const buffer = Buffer.from('test'); keys(buffer); // => ['0', '1', '2', '3'] // TypedArray 不应包含 buffer、byteLength、byteOffset 键 const typedArray = new Uint8Array(1); keys(typedArray); // => ['0']此外,源码还会把稀疏数组当作稠密数组处理:[1, , 3]这类存在空洞的数组也会返回['0', '1', '2'](见 测试用例),并且数组上挂载的自定义属性同样会被纳入结果(['0', 'a'],见 测试用例)。
六、null 与 undefined 的安全处理
keys对空值非常宽容,传入null或undefined不会抛出异常,而是安全返回空数组:
import { keys } from 'es-toolkit/compat'; keys(null); // => [] keys(undefined); // => []在测试中(src/compat/object/keys.spec.ts),这一行为甚至在Object.prototype上挂载了额外属性时依然成立。对于字符串以外的原始值(数字、布尔等),keys会将其装箱为对象后再处理:例如keys(0)返回[]、keys('a')返回['0'],这也与 IE 9 时代的 Lodash 兼容测试保持一致(见 测试用例)。
七、原型对象:跳过constructor属性
当传入的对象本身是一个原型对象(如Foo.prototype)时,源码会额外过滤掉constructor键。这一判断依赖内部工具函数 isPrototype:
export function isPrototype(value: object) { const constructor = value?.constructor; const prototype = typeof constructor === 'function' ? constructor.prototype : Object.prototype; return value === prototype; }即:如果传入值恰好等于某个构造函数的prototype(或等于Object.prototype),就判定为原型对象,随后在主函数中过滤掉constructor:
// 主函数中的关键分支(src/compat/object/keys.ts#L31-L37) const result = Object.keys(Object(object)); if (!isPrototype(object)) { return result; } return result.filter(key => key !== 'constructor');对应的测试(src/compat/object/keys.spec.ts)验证了keys(Foo.prototype)返回['a']而不包含constructor,同时keys({ constructor: Foo, a: 1 })也会排除constructor。
八、源码级实现解析:完整执行流程
结合以上内容,可以梳理出 keys 主函数 的完整执行流程:
keys(object) ├─ 1. isArrayLike(object) 为 true? │ └─ 是 → 走 arrayLikeKeys 分支(类数组专用逻辑) │ ├─ 生成 0..length-1 索引序列 │ ├─ Buffer → 过滤 offset/parent │ ├─ TypedArray → 过滤 buffer/byteLength/byteOffset │ ├─ 合并非索引自身属性 │ └─ 非数组时用 Object.hasOwn 校验索引 │ └─ 2. 否则: ├─ Object.keys(Object(object)) 取自身可枚举属性 └─ isPrototype(object) 为 true? └─ 是 → 过滤掉 'constructor' └─ 否 → 直接返回可以看到,keys相比Object.keys()多出的开销主要集中在:类数组判定(isArrayLike)、索引序列生成与Set构建、Buffer/TypedArray 兼容分支、原型对象判定(isPrototype)以及最终的过滤操作。这正是官方文档提示「该函数运行较慢,建议改用Object.keys()」的底层原因——这些兼容逻辑是有成本的,普通对象场景下原生 API 完全没有这些负担。
九、与keysIn的对比:是否包含继承属性
es-toolkit 兼容层还提供了keysIn函数(src/compat/object/keysIn.ts),它与keys的最大区别是:keysIn会包含原型链上的可枚举继承属性,而keys只返回自身属性。
import { keysIn } from 'es-toolkit/compat'; function Foo() {} Foo.prototype.a = 1; console.log(keysIn(new Foo())); // ['a'](包含继承属性)两者在类数组对象、Buffer、TypedArray、原型对象的处理策略上保持一致(keysIn同样会过滤 Buffer 的offset/parent与 TypedArray 的buffer/byteLength/byteOffset,同样跳过原型对象的constructor),区别仅在于遍历范围:keysIn使用for...in遍历(会沿原型链向上),keys则基于Object.keys只取自身属性。如果迁移自 Lodash 的_.keysIn,请对应使用keysIn,不要与keys混用。
十、实战建议:何时用keys,何时用Object.keys
综合官方文档的警告与源码分析,可以给出如下选型建议:
| 场景 | 推荐 API | 理由 |
|---|---|---|
| 新代码、无 Lodash 迁移负担 | Object.keys() | 无兼容逻辑,性能更好 |
| 从 Lodash 迁移、追求行为完全一致 | keys(es-toolkit/compat) | 完整兼容类数组、原型对象、空值等边界行为 |
| 需要包含继承属性 | keysIn(es-toolkit/compat) | 对应 Lodash 的_.keysIn |
| 需要键值对数组 | toPairs/entries | 键值场景有更直接的 API |
keys的核心价值在于行为兼容性:稀疏数组按稠密处理、字符串按索引展开、TypedArray/Buffer 过滤特殊属性、原型对象跳过constructor、空值安全返回[]——这些细节都是Object.keys()不具备或行为不同的地方。如果你的代码依赖这些 Lodash 语义,keys就是迁移时的可靠替代;否则请遵循官方建议,直接使用原生Object.keys()。
十一、进一步阅读
- 函数实现:src/compat/object/keys.ts
- 测试用例(完整覆盖边界行为):src/compat/object/keys.spec.ts
- 原型对象判定工具:src/compat/_internal/isPrototype.ts
- 包含继承属性的兄弟函数:src/compat/object/keysIn.ts
- 兼容层统一导出:src/compat/compat.ts
- 英文原版参考文档:docs/compat/reference/object/keys.md
【免费下载链接】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),仅供参考