es-toolkit 兼容层 keysIn 详解:获取含继承属性的全部枚举键名
【免费下载链接】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
keysIn是 es-toolkit 兼容层(es-toolkit/compat)为对齐 lodash 而提供的对象工具函数,它返回对象自身与原型链上所有可枚举属性名的字符串数组。本文将以 keysIn 兼容文档 为骨架,结合 keysIn 源码 与其 完整测试用例,讲解该函数的基本用法、对数组 / 类数组 / 继承属性 / 空值的特殊处理,以及其底层实现原理与性能注意事项,帮助你在迁移 lodash 代码或日常取键场景中做出正确选择。
功能概览:keysIn 解决什么问题
keysIn的作用是:返回一个对象的所有可枚举属性名(包括自身属性与继承属性)组成的字符串数组。
const allKeys = keysIn(object);它与keys的核心区别在于:keys(见 keys 源码)只返回自身可枚举属性,而keysIn会沿着原型链一路向上,把继承到的可枚举属性也一并纳入结果。这与 lodash 的_.keysIn行为保持一致,方便 lodash 老项目平滑迁移。
注意:该函数仅返回字符串键的属性名(Symbol 键不在其中),且与 lodash 相同,结果数组不保证键的迭代顺序。
基本用法
在es-toolkit/compat入口下导入即可使用:
import { keysIn } from 'es-toolkit/compat'; // 基本对象的键 const object = { a: 1, b: 2 }; keysIn(object); // => ['a', 'b'] // 数组的索引 const array = [1, 2, 3]; keysIn(array); // => ['0', '1', '2'] // 字符串的索引 keysIn('hello'); // => ['0', '1', '2', '3', '4']值得注意的两个细节:
- 数组返回的是字符串形式的索引
'0'、'1'、'2',而不是数字下标; - 字符串原语会被自动装箱为 String 对象,从而返回字符位置索引。这一点在 keysIn.spec.ts 中有专门用例验证,且字符串对象上自定义的属性(如
Object('a').a = 1)也会被返回。
参数与返回值
- 参数
object(any):要获取键的对象。 - 返回值(
string[]):包含自身与继承的所有可枚举属性名(字符串键)的数组。
继承属性:与 keys 的本质差异
keysIn会包含原型链上可枚举的继承属性,这是它区别于keys的最关键行为:
import { keysIn } from 'es-toolkit/compat'; function Foo() { this.a = 1; this.b = 2; } Foo.prototype.c = 3; keysIn(new Foo()); // => ['a', 'b', 'c'](包含原型属性 'c') // constructor 会被排除 class MyClass { constructor() { this.prop = 1; } method() {} } MyClass.prototype.inherited = 2; keysIn(new MyClass()); // => ['prop', 'method', 'inherited'](constructor 被排除)两个要点需要特别说明:
- 原型上的普通属性会被包含:
Foo.prototype.c = 3后,实例new Foo()的键自然多出'c'。 constructor被排除:当传入的对象本身是一个原型对象(prototype)时,结果中不会出现constructor。测试用例 keysIn.spec.ts 覆盖了多种原型场景,包括Foo.prototype、显式赋值{ constructor: Foo, a: 1 },以及伪造的Fake.prototype,验证constructor的过滤逻辑仅在原型对象上生效。
这一"排除 constructor"的行为并非在普通对象上硬性过滤,而是由源码中的isPrototype判定触发(见下文源码剖析)。
类数组对象的特殊处理
keysIn对类数组(array-like)对象走专门的分支逻辑,与普通对象不同:
import { keysIn } from 'es-toolkit/compat'; // TypedArray const typedArray = new Uint8Array([1, 2, 3]); keysIn(typedArray); // => ['0', '1', '2'](buffer、byteLength 等被排除) // arguments 对象 function example() { return keysIn(arguments); } example('a', 'b', 'c'); // => ['0', '1', '2']行为细节包括:
- TypedArray:会剔除
buffer、byteLength、byteOffset这些"非索引"的枚举属性(对应测试 keysIn.spec.ts); - arguments 对象:返回
['0', '1', '2'],若在 arguments 上添加自定义属性a,结果会变为['0', '1', '2', 'a'](见 keysIn.spec.ts); - 稀疏数组按稠密处理:
const arr = [1]; arr[2] = 3;会返回['0', '1', '2'],缺失的下标也被补齐(keysIn.spec.ts); - 类数组对象保留 length:
{ 0: 'a', length: 1 }返回['0', 'length'](keysIn.spec.ts); - Buffer:不会返回
offset、parent(keysIn.spec.ts); - 数组上的自定义属性也会保留:
[1].a = 1返回['0', 'a'],且Array.prototype.a = 1这类继承属性同样会被包含(keysIn.spec.ts)。
null / undefined 的安全处理
与 lodash 一致,keysIn对空值不会抛错,而是返回空数组:
import { keysIn } from 'es-toolkit/compat'; keysIn(null); // => [] keysIn(undefined); // => []测试 keysIn.spec.ts 还额外验证了:即使此时在Object.prototype上挂载了属性(模拟污染环境),keysIn(null)、keysIn(undefined)以及无参调用依然稳定返回[],不会受原型污染干扰。
源码实现剖析
从 keysIn 源码 可以看到,keysIn采用按类型分流的策略实现:
export function keysIn(object?: any): string[] { if (object == null) { return []; } switch (typeof object) { case 'object': case 'function': { if (isArrayLike(object)) { return arrayLikeKeysIn(object); } if (isPrototype(object)) { return prototypeKeysIn(object); } return keysInImpl(object); } default: { return keysInImpl(Object(object)); } } }核心路径可拆解为四层:
- 空值短路:
object == null直接返回[]; - 类数组分支:通过 isArrayLike(基于
length是否为合法长度且非函数)判定后进入arrayLikeKeysIn; - 原型对象分支:通过 isPrototype(比对
value是否等于其constructor.prototype)判定后进入prototypeKeysIn,过滤掉constructor键; - 默认分支:
keysInImpl直接使用for...in循环收集所有可枚举键(含继承),而对原始类型(数字、字符串、布尔、Symbol、BigInt)则先执行Object(object)装箱转换。
在arrayLikeKeysIn(keysIn.ts)内部,先用times生成全部下标字符串,再用Set建立"应排除的键集合"(对 Buffer 额外加入offset、parent,对 TypedArray 额外加入buffer、byteLength、byteOffset),随后通过for...in收集继承键并去重,最后按"是否为真数组"决定是否用Object.hasOwn过滤缺失下标。整个设计目标是在 lodash 兼容语义(稠密化、排除内建非索引属性、含继承键)与可读性之间取得平衡。
性能注意事项:何时不该用它
官方文档在函数开头就给出了明确的警告——建议优先使用for...in循环或Object.keys():
keysIn由于包含类数组处理、原型链遍历等复杂逻辑,运行速度较慢。请改用更快的现代写法:for...in循环,或按需使用Object.keys()。
在 keysIn 源码 中可以看到,keysInImpl本质上就是for...in,但外层还叠加了isArrayLike、isPrototype、isTypedArray、isBuffer等多重判定,以及下标数组生成与Set去重,这些都会带来额外开销。因此在追求性能的热路径代码中:
- 只需自身键 → 用
Object.keys(obj); - 需要含继承的可枚举键 → 直接写
for (const key in obj)循环。
keysIn更适合用于与 lodash 逐字节对齐的兼容场景,或需要其类数组特殊语义(如 TypedArray 排除buffer等)的场合。
与相关函数的关系
在 es-toolkit 兼容层中,keysIn与以下函数构成一个"取键/取值"家族,可组合使用:
keys:只返回自身可枚举键(见 keys 文档 与 keys 源码),内部同样复用了arrayLikeKeys与isPrototype逻辑,区别仅在于继承属性的取舍;valuesIn:返回含继承属性的所有可枚举值(valuesIn.ts);toPairsIn:返回含继承属性的[key, value]键值对数组(toPairsIn.ts);assignIn/assignInWith:合并对象时同样遍历含继承的可枚举属性(assignIn.ts);pickBy/omit/omitBy:在过滤属性时也会用到含继承键的遍历逻辑(pickBy.ts)。
这些函数共同构成了"全量遍历属性(own + inherited)"的兼容语义族。当你在迁移 lodash 代码时发现_.keysIn、_.valuesIn、_.toPairsIn等成对出现,可以直接用 es-toolkit 的对应函数无缝替换。
小结
keysIn是 es-toolkit 兼容层中语义最"宽"的取键函数:它返回自身 + 原型链上的全部可枚举字符串键,对数组、字符串、arguments、TypedArray 等类数组输入做专门的稠密化与属性过滤处理,对null/undefined返回空数组,并对原型对象排除constructor。其源码通过"空值短路 → 类数组分支 → 原型分支 → 默认for...in"的四级分流,在 lodash 兼容行为与现代实现之间做了清晰切分。日常开发中,除非需要与 lodash 行为严格对齐或依赖其类数组特殊语义,否则更推荐直接使用for...in或Object.keys()以获取更好的性能。
【免费下载链接】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),仅供参考