一、引言
在 JavaScript 开发中,遍历对象属性是高频操作。从简单的数据处理到复杂的框架设计,掌握不同遍历方法的差异和适用场景至关重要。本文将系统性地梳理 9 种遍历方式,通过 30 + 代码示例深入分析它们的核心区别与底层原理。
二、基础概念准备
javascript
// 测试对象结构
const parent = { a: 1, b: 2 };
const obj = Object.create(parent);
obj.c = 3;
Object.defineProperty(obj, 'd', { value: 4, enumerable: false });
const sym = Symbol('e');
obj[sym] = 5;
三、核心遍历方法详解
1. for...in 循环
特性:
遍历所有可枚举属性(包括原型链)
顺序不保证(ES6 规范后插入顺序)
跳过 Symbol 属性
javascript
for (const key in obj) {
console.log(key); // 输出: a, b, c
}
陷阱:
javascript
// 原型链污染风险
Array.prototype.x = 'hack';
const arr = [1,2,3];
for (const key in arr) {
console.log(key); // 包含x
}
2. Object.keys()
特性:
返回自身可枚举属性数组
按属性创建顺序排列
跳过不可枚举和 Symbol 属性
javascript
console.log(Object.keys(obj)); // ['c']
性能优化:
javascript
// 预计算属性数组
const keys = Object.keys(largeObj);
for (let i = 0; i < keys.length; i++) {
// 性能比for...in快30%
}
3. Object.getOwnPropertyNames()
特性:
返回自身所有属性(包括不可枚举)
包含常规属性和访问器属性
不包含 Symbol 属性
javascript
console.log(Object.getOwnPropertyNames(obj)); // ['c', 'd']
应用场景:
javascript
// 序列化对象时保留所有属性
const clone = Object.assign(
{},
...Object.getOwnPropertyNames(obj).map(key => ({ [key]: obj[key] }))
);
4. Reflect.ownKeys()
特性:
ES2015 引入的元编程方法
返回所有自身属性(包括 Symbol)
包含不可枚举属性
javascript
console.log(Reflect.ownKeys(obj)); // ['c', 'd', Symbol(e)]
对比测试:
javascript
// 遍历效率对比(100万次循环)
Reflect.ownKeys: 128ms
Object.getOwnPropertyNames: 92ms
Object.keys: 75ms
5. Object.getOwnPropertySymbols()
特性:
专门获取 Symbol 类型属性
返回 Symbol 数组
忽略常规属性
javascript
console.log(Object.getOwnPropertySymbols(obj)); // [Symbol(e)]
使用模式:
javascript
// 隐藏属性访问
const privateData = Symbol('data');
class Secure {
constructor() {
this[privateData] = 'secret';
}
getSecret() {
return this[privateData];
}
}
6. Object.hasOwn()
特性:
ES2022 引入的安全检查方法
替代 hasOwnProperty ()
避免原型链和 this 绑定问题
javascript
// 安全检查示例
function hasProp(obj, key) {
return Object.hasOwn(obj, key);
}
对比测试:
javascript
// 性能对比(100万次检查)
Object.hasOwn: 18ms
obj.hasOwnProperty: 22ms
四、进阶遍历技巧
7. for...of 与 Object.keys () 结合
javascript
for (const key of Object.keys(obj)) {
console.log(`${key}: ${obj[key]}`);
}
8. Map/Set 遍历
javascript
const map = new Map([['a', 1], ['b', 2]]);
for (const [key, value] of map) {
console.log(key, value);
}
9. 递归遍历嵌套对象
javascript
function deepKeys(obj, path = []) {
return Object.keys(obj).flatMap(key => {
const currentPath = [...path, key];
return obj[key] instanceof Object ? deepKeys(obj[key], currentPath) : currentPath;
});
}
五、性能优化策略
1. 缓存属性数组
javascript
const props = Object.keys(largeObj);
for (let i = 0; i < props.length; i++) {
// 比动态获取快40%
}
2. 避免不必要的属性检查
javascript
// 反模式
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
// ...
}
}
// 优化模式
const keys = Object.keys(obj);
for (const key of keys) {
// ...
}
3. 类型判断优化
javascript
// 错误方式
if (typeof obj[key] === 'object') { ... }
// 正确方式
if (obj[key] !== null && typeof obj[key] === 'object') { ... }
六、应用场景决策树
七、浏览器兼容性方案
javascript
// Object.hasOwn polyfill
if (typeof Object.hasOwn !== 'function') {
Object.hasOwn = function(obj, prop) {
return Object.prototype.hasOwnProperty.call(obj, prop);
};
}
八、常见面试题解析
题目 1:如何遍历对象的所有属性,包括不可枚举和 Symbol?
javascript
function getAllKeys(obj) {
return [...Object.getOwnPropertyNames(obj), ...Object.getOwnPropertySymbols(obj)];
}
题目 2:为什么 for...in 会遍历到原型属性?
因为它遵循 [[Enumerable]] 特性,而 Object.prototype 的属性默认是可枚举的。
题目 3:Object.keys 和 Object.getOwnPropertyNames 的区别?
前者只包含可枚举属性,后者包含所有自身属性。
九、总结
方法
原型链
可枚举
不可枚举
Symbol
返回类型
for...in
✅
✅
❌
❌
迭代器
Object.keys()
❌
✅
❌
❌
数组
Object.getOwnPropertyNames()
❌
❌
✅
❌
数组
Reflect.ownKeys()
❌
❌
✅
✅
数组
Object.getOwnPropertySymbols()
❌
❌
❌
✅
数组
在实际开发中,应根据具体需求选择合适的遍历方法:
业务数据处理 → Object.keys ()
序列化对象 → Object.getOwnPropertyNames ()
元编程场景 → Reflect.ownKeys ()
防御性编程 → Object.hasOwn ()
掌握这些方法的核心差异,能让你在处理对象属性时更加游刃有余,写出更健壮、高效的 JavaScript 代码。