JS遍历对象属性的5种常用方法:for...in与Object.keys对比
系统解析JavaScript中遍历对象属性的5种常用方法:for...in、Object.keys、Object.getOwnPropertyNames、Object.getOwnPropertySymbols与Reflect.ownKeys。深度对比for...in与Object.keys在原型链继承、枚举特性、执行效率与安全机制上的核心差异,附实操决策流程与对比矩阵。
for...in、Object.keys()、Object.getOwnPropertyNames()、Object.getOwnPropertySymbols() 以及 Reflect.ownKeys()。其中 for...in 与 Object.keys() 在业务中使用最频繁,也最容易因原型链渗透或返回值差异引发隐蔽缺陷。本文将逐一阐明这 5 种方法的行为边界,并对 for...in 与 Object.keys() 进行深度剖析与选型对照。
一、理解 JavaScript 对象的属性分类
要准确选择遍历方式,必须先明确 JavaScript 引擎如何在内存中标记一个属性。任何一个附加在对象上的属性,都由以下三个维度共同决定其能否被某种方法访问到:
- 归属维度(自身 vs 继承):是对象直接拥有的“自身属性”(Own Property),还是通过隐式原型链
__proto__继承自祖先对象的“原型属性”(Inherited Property)。 - 枚举维度(可枚举 vs 不可枚举):属性描述符中的
enumerable标志。默认通过字面量obj.a = 1声明的属性是可枚举的(true),而通过Object.defineProperty默认创建的属性或原型上大多数内置方法(如toString)是不可枚举的(false)。 - 类型维度(字符串 vs 符号):属性名是常规的字符串类型,还是 ES6 引入的
Symbol唯一符号。
为了直观展示后续 5 种方法的输出差异,我们准备一个包含所有上述维度属性的测试对象:
// 1. 定义原型对象,包含一个可枚举属性和一个不可枚举属性
const proto = {
protoEnumberable: '继承的可枚举属性'
};
Object.defineProperty(proto, 'protoNonEnumerable', {
value: '继承的不可枚举属性',
enumerable: false
});
// 2. 以 proto 为原型创建实例对象
const targetObj = Object.create(proto);
// 3. 添加自身常规可枚举属性
targetObj.title = 'JavaScript 核心进阶';
targetObj.views = 1024;
// 4. 添加自身不可枚举属性
Object.defineProperty(targetObj, 'internalSecret', {
value: '内部版本 v2.0',
enumerable: false,
writable: true
});
// 5. 添加自身 Symbol 属性
const symbolKey = Symbol('metaToken');
targetObj[symbolKey] = 'secret_symbol_token_998';
targetObj 拥有: - 自身可枚举字符串属性:'title', 'views' - 自身不可枚举字符串属性:'internalSecret' - 自身 Symbol 属性:[Symbol(metaToken)] - 原型链可枚举属性:'protoEnumberable' - 原型链不可枚举属性:'protoNonEnumerable'
二、5 种常用遍历方法详解
1. for...in 循环
for...in 是 JavaScript 最早支持的遍历语法。它的核心特征是:遍历目标对象自身以及整条原型链上的所有可枚举字符串属性。它不会遍历不可枚举属性,也不会遍历 Symbol 属性。
// 示例:使用 for...in 遍历 targetObj
const forInKeys = [];
for (const key in targetObj) {
forInKeys.push(key);
}
console.log(forInKeys);
[ 'title', 'views', 'protoEnumberable' ]
protoEnumberable!如果第三方库或项目代码在 Object.prototype 上挂载了自定义方法,for...in 会将它们一并遍历出来。因此在遍历自身属性时,必须加上过滤防御。
2. Object.keys()
Object.keys() 接收一个对象作为参数,仅返回对象自身拥有的可枚举字符串属性名称构成的数组。它不会沿着原型链向上查找,也不会包含不可枚举属性或 Symbol 属性。
// 示例:使用 Object.keys() 遍历 targetObj
const ownKeys = Object.keys(targetObj);
console.log(ownKeys);
// 配合 forEach 遍历键值
ownKeys.forEach(key => {
console.log(`${key}: ${targetObj[key]}`);
});
[ 'title', 'views' ] // 对应打印: title: JavaScript 核心进阶 views: 1024
Object.values()(仅返回自身可枚举属性值数组)和 Object.entries()(返回 [key, value] 键值对数组),两者的属性筛选边界与 Object.keys() 完全一致。
3. Object.getOwnPropertyNames()
Object.getOwnPropertyNames() 返回一个数组,包含对象自身所有的字符串属性键名(包括可枚举与不可枚举属性),但同样不包含 Symbol 属性,也不查找原型链。
// 示例:使用 Object.getOwnPropertyNames() 探查自身所有字符串属性
const allStringKeys = Object.getOwnPropertyNames(targetObj);
console.log(allStringKeys);
[ 'title', 'views', 'internalSecret' ]
可以看到,不可枚举属性 internalSecret 被成功读取,而原型链上的属性和 Symbol 属性依然被过滤。这个方法常用于深度调试、查看类实例上的内置方法或属性描述符提取。
4. Object.getOwnPropertySymbols()
由于常规方法均会自动忽略 Symbol 类型的属性名,ECMAScript 专门提供了 Object.getOwnPropertySymbols()。它专门返回对象自身所拥有的所有 Symbol 属性键,无论该 Symbol 是否设置了 enumerable: true。
// 示例:读取自身 Symbol 属性
const symbolKeys = Object.getOwnPropertySymbols(targetObj);
console.log(symbolKeys);
console.log(targetObj[symbolKeys[0]]);
[ Symbol(metaToken) ] secret_symbol_token_998
5. Reflect.ownKeys()
Reflect.ownKeys() 是 ES6 Proxy 与反射规范中功能最全面的键提取方法。它相当于 Object.getOwnPropertyNames() 与 Object.getOwnPropertySymbols() 的合集:返回目标对象自身的所有键(包括字符串键与 Symbol 键,包括可枚举与不可枚举属性),但不包含原型链属性。
// 示例:Reflect.ownKeys 完整获取所有自身键
const reflectKeys = Reflect.ownKeys(targetObj);
console.log(reflectKeys);
[ 'title', 'views', 'internalSecret', Symbol(metaToken) ]
在实现深拷贝(Deep Clone)、对象代理拦截或底层序列化库时,Reflect.ownKeys() 是确保所有私有标识和配置不遗漏的唯一完备方案。
三、深度对比:for...in 与 Object.keys()
尽管 for...in 和 Object.keys() 在普通平铺对象上的直观行为类似,但在语言规范层面,它们存在四个维度的本质差异。
1. 原型链穿透与安全机制
for...in 在遍历时会沿着原型链一层一层向上回溯,直到 Object.prototype 为止。如果在开发过程中使用了扩展原型的第三方库,或者原型链存在继承数据,for...in 会不可避免地读取到原型数据。
若必须使用 for...in 仅处理自身属性,必须手动加入 Object.hasOwn() 防御(ES2022 推荐做法,取代容易被重写的 hasOwnProperty):
// 规范的 for...in 防御写法
for (const key in targetObj) {
if (Object.hasOwn(targetObj, key)) {
console.log(`自身属性: ${key} = ${targetObj[key]}`);
}
}
而 Object.keys() 在底层直接通过 [[OwnPropertyKeys]] 获取集合,从机制上物理隔离了原型链,无需任何额外的防范逻辑,代码简洁且无原型污染风险。
2. 语法结构与返回形式
for...in是一种流程控制语句,它在迭代过程中依次抛出键名。这意味着它支持在循环体内直接使用break、continue、return(函数内)实现随时中断。Object.keys()是一个静态方法,它一次性在内存中生成一个包含了所有键名的标准 Array 快照。生成数组后,可以无缝调用数组的高阶函数,例如.map()、.filter()、.reduce()或for...of。
3. 遍历性能与内存考量
在遍历大型对象或频繁触发热点循环的场景下:
for...in每次步进都需要动态检查当前对象及原型链上的属性描述符,现代 JavaScript 引擎(如 V8)对原型链查询的优化成本远高于单纯的自身数组遍历。Object.keys()虽需在初始化时分配数组内存,但由于其范围严格限定于自身对象,V8 的 Fast Properties(隐藏类 Hidden Class)机制能极快完成收集,后续在纯连续内存数组上运行迭代速度极快。
4. 遍历顺序规范
在 ECMAScript 2015 之前,for...in 的遍历顺序在规范中属于“依赖具体引擎实现”,不同浏览器可能存在差异。从 ES6 开始,规范定义了确定性的遍历次序(无论是 Object.keys() 还是 Reflect.ownKeys() 均遵循):
- 首先遍历所有非负整数索引属性,按升序数值顺序排列(例如
"0", "1", "2"); - 其次遍历所有常规字符串键,按创建/赋值的时间插入顺序排列;
- 最后遍历所有 Symbol 键(若当前方法支持),按创建插入顺序排列。
然而,for...in 在涉及原型链上属性时,先完整遍历当前对象的属性,再去遍历原型对象上的属性。若两者存在相同数字索引,其行为复杂度显著增加。
四、如何选择?遍历方法决策与操作步骤
在编写业务逻辑或封装通用库时,应根据对属性范围的明确诉求选择对应方法。下图展示了从需求到选型的实操判定步骤:
实操判定四步骤:
-
步骤 1:判断是否涉及 Symbol?
如果目标对象是带有元数据、防命名冲突的组件配置或 Proxy 代理对象,常规方法均无法获取 Symbol。若只需要 Symbol,使用
Object.getOwnPropertySymbols();若需要全部键,直接选用Reflect.ownKeys()。 -
步骤 2:判断是否需要不可枚举属性?
常规业务对象不需要不可枚举属性。但如果是调试框架代码、查看类原型上的隐式配置,需使用
Object.getOwnPropertyNames()。 -
步骤 3:判断是否需要沿着原型链向上获取?
如果正在维护老旧基于原型继承的项目,或者确实需要提取继承得到的配置项,才应考虑
for...in,且必须使用Object.hasOwn()过滤不需要的顶层原型成员。 -
步骤 4:确认默认首选方案
如果以上特殊条件均不满足(即绝大多数前端 API 响应处理、状态机数据传递、表单键值提取场景),无条件首选
Object.keys()或Object.entries()。
五、总结与核心特性对照
为了方便随时查阅与复习,下表和矩阵图总结了这 5 种遍历方法在自身可枚举、不可枚举、原型链、Symbol 属性及返回值形式上的全景特征:
| 方法名称 | 自身可枚举 | 自身不可枚举 | 原型链继承 | Symbol 键 | 返回值与迭代机制 | 典型适用场景 |
|---|---|---|---|---|---|---|
for...in |
✔ 是 | ✖ 否 | ✔ 是 (可枚举) | ✖ 否 | 语句迭代 (需防范原型污染) | 需穿透原型链的旧继承对象扫描 |
Object.keys() |
✔ 是 | ✖ 否 | ✖ 否 | ✖ 否 | 字符串数组 string[] |
日常业务开发默认推荐首选 |
Object.getOwnPropertyNames() |
✔ 是 | ✔ 是 | ✖ 否 | ✖ 否 | 字符串数组 string[] |
排查内部属性、类实例方法提取 |
Object.getOwnPropertySymbols() |
✖ 否 | ✖ 否 | ✖ 否 | ✔ 仅 Symbol | 符号数组 symbol[] |
读取私有 Symbol 标识与元数据 |
Reflect.ownKeys() |
✔ 是 | ✔ 是 | ✖ 否 | ✔ 是 (全部) | 混合数组 (string | symbol)[] |
完整深克隆、Proxy 拦截与元编程 |
开发实战建议清单:
- 切勿使用
for...in遍历数组:for...in会将数组原型上的自定义扩展名一并作为字符串键读出,且无法保证纯数值索引顺序。遍历数组请统一使用for...of、Array.prototype.forEach()或标准下标for循环。 - 优先使用
Object.entries()替代手动取值:如果遍历对象既需要键又需要值,使用for (const [key, val] of Object.entries(obj))更加简洁直观,避免重复的obj[key]寻址。 - 防御性校验不可忽视:在处理外部未知来源的字典数据时,若由于历史原因使用了
for...in,务必使用Object.hasOwn(obj, key)进行安全守卫。
JS中将数组和对象混合组装为JSON字符串
2026-11-05 16:41:42
JS运算符优先级与结合性的区别是什么?
2026-11-05 13:22:02
JS Map怎么只遍历Key或Value
2026-11-05 13:00:36
D3.js 与 ECharts 选型对比:灵活性 vs 开发效率
2026-11-05 09:45:01
为什么JS中typeof []返回object:数组类型检测误区解析
2026-11-04 19:50:47
相关标签:






![JS输出对象显示[object Object]怎么办?JSON.stringify格式化打印技巧](https://img.javascript.cn/tit/articles/13236/20261105/6eaec9baed78171fe0bc2b74184ba5b7b80ebd52de7d3323ff152268aa2df294.webp)

