JS遍历对象属性的5种常用方法:for...in与Object.keys对比

作者:木棉

时间:2026-11-03

浏览:101

系统解析JavaScript中遍历对象属性的5种常用方法:for...in、Object.keys、Object.getOwnPropertyNames、Object.getOwnPropertySymbols与Reflect.ownKeys。深度对比for...in与Object.keys在原型链继承、枚举特性、执行效率与安全机制上的核心差异,附实操决策流程与对比矩阵。

文章目录
分类:JavaScript核心 阅读时长:约 8 分钟 标准版本:ECMAScript 2015+ (ES6+)
JS遍历对象属性的5种常用方法:for...in与Object.keys对比
图 0:JavaScript 对象属性遍历 5 大常用方法特性速览
在 JavaScript 中遍历一个普通对象并非只有“取到所有键”这一种简单结果。对象的键存在三大关键维度:自身属性与原型链继承属性、可枚举性(enumerable)以及键类型(字符串键与 Symbol 键)。目前语言内置了 5 种常用的属性获取与遍历方案: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 唯一符号。
JS遍历对象属性的5种常用方法:for...in与Object.keys对比
图 1:JavaScript 对象属性划分与各遍历方法覆盖范围

为了直观展示后续 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
补充说明:ES2017 还引入了 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() 在普通平铺对象上的直观行为类似,但在语言规范层面,它们存在四个维度的本质差异。

JS遍历对象属性的5种常用方法:for...in与Object.keys对比
图 2: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() 均遵循):

  1. 首先遍历所有非负整数索引属性,按升序数值顺序排列(例如 "0", "1", "2");
  2. 其次遍历所有常规字符串键,按创建/赋值的时间插入顺序排列;
  3. 最后遍历所有 Symbol 键(若当前方法支持),按创建插入顺序排列。

然而,for...in 在涉及原型链上属性时,先完整遍历当前对象的属性,再去遍历原型对象上的属性。若两者存在相同数字索引,其行为复杂度显著增加。

四、如何选择?遍历方法决策与操作步骤

在编写业务逻辑或封装通用库时,应根据对属性范围的明确诉求选择对应方法。下图展示了从需求到选型的实操判定步骤:

JS遍历对象属性的5种常用方法:for...in与Object.keys对比
图 3:属性遍历方法选择与实操步骤流程图

实操判定四步骤:

  1. 步骤 1:判断是否涉及 Symbol? 如果目标对象是带有元数据、防命名冲突的组件配置或 Proxy 代理对象,常规方法均无法获取 Symbol。若只需要 Symbol,使用 Object.getOwnPropertySymbols();若需要全部键,直接选用 Reflect.ownKeys()。
  2. 步骤 2:判断是否需要不可枚举属性? 常规业务对象不需要不可枚举属性。但如果是调试框架代码、查看类原型上的隐式配置,需使用 Object.getOwnPropertyNames()。
  3. 步骤 3:判断是否需要沿着原型链向上获取? 如果正在维护老旧基于原型继承的项目,或者确实需要提取继承得到的配置项,才应考虑 for...in,且必须使用 Object.hasOwn() 过滤不需要的顶层原型成员。
  4. 步骤 4:确认默认首选方案 如果以上特殊条件均不满足(即绝大多数前端 API 响应处理、状态机数据传递、表单键值提取场景),无条件首选 Object.keys() 或 Object.entries()。

五、总结与核心特性对照

为了方便随时查阅与复习,下表和矩阵图总结了这 5 种遍历方法在自身可枚举、不可枚举、原型链、Symbol 属性及返回值形式上的全景特征:

JS遍历对象属性的5种常用方法:for...in与Object.keys对比
图 4:5 种对象属性遍历方法核心特性全景对比总结矩阵
方法名称 自身可枚举 自身不可枚举 原型链继承 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) 进行安全守卫。

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章