创见博客
对象的遍历:方法对比与选型
七崽爱吃小饼干2026/10/07阅读 0

数组遍历有 forEach、for...of 一整套,对象就尴尬了——for...of 直接用会报错:

js
const obj = { a: 1, b: 2 }
for (const v of obj) {} // TypeError: obj is not iterable

对象不是可迭代对象,想遍历它得挑对方法。而坑在于:对象的键分可枚举 / 不可枚举、自有 / 继承、字符串 / Symbol,不同方法覆盖的范围完全不同。这篇先把这些维度理清,再逐个方法对比,最后给选型建议。

一、先搞清三个维度

遍历对象时,属性可以按三个维度分类:

  1. 自有属性(own) vs 继承属性(inherited):原型链上的算不算?
  2. 可枚举(enumerable) vs 不可枚举:Object.defineProperty 设为不可枚举的属性要不要?
  3. 字符串键 vs Symbol 键:Symbol 类型的键要不要?

一个属性是不是"可枚举",决定了 for...in、Object.keys 这些方法是否能看到它。

js
const obj = { name: 'Tom' }

Object.defineProperty(obj, 'age', {
  value: 18,
  enumerable: false, // 不可枚举
})

obj[Symbol('id')] = 1 // Symbol 键

Object.keys(obj)  // ['name'],看不到 age,也看不到 Symbol

下面所有方法,本质上都是在"上面这三个维度"里各取一个子集。

二、for...in:会带上继承属性

for...in 遍历对象自身及原型链上所有可枚举的属性(不含 Symbol):

js
const parent = { inherited: 'p' }
const obj = Object.create(parent)
obj.own = 'o'

for (const key in obj) {
  console.log(key) // "own"、"inherited"
}

正因为会遍历到原型链上的东西,通常要配合 hasOwnProperty 过滤:

js
for (const key in obj) {
  if (Object.hasOwn(obj, key)) { // 或 obj.hasOwnProperty(key)
    console.log(key, obj[key])
  }
}

for...in 的优点是能遍历,缺点是键不保证顺序(虽然现代引擎已有规则),且顺带遍历原型。不建议在业务代码里裸用。

三、Object.keys / values / entries:最常用

这三个返回的都是自有、可枚举的字符串键,不含 Symbol,也不含原型链:

js
const obj = { a: 1, b: 2 }

Object.keys(obj)    // ['a', 'b']
Object.values(obj)  // [1, 2]
Object.entries(obj) // [['a', 1], ['b', 2]]

因为返回数组,可以无缝接上数组方法:

js
Object.entries(obj).forEach(([key, value]) => {
  console.log(key, value)
})

// 对象转 Map
const map = new Map(Object.entries(obj))

日常遍历对象,这三个基本够用。注意 Object.values / Object.entries 是 ES2017 才有的,老环境要做兼容。

四、拿到 Symbol 键和不可枚举属性

上面三个方法都拿不到 Symbol 键和不可枚举属性。需要更全的:

js
const sym = Symbol('s')
const obj = { a: 1, [sym]: 2 }

Object.getOwnPropertyNames(obj)  // ['a'],自有字符串键(含不可枚举)
Object.getOwnPropertySymbols(obj) // [Symbol(s)],自有 Symbol 键
Reflect.ownKeys(obj)             // ['a', Symbol(s)],两者合并,最全

对比:

方法字符串键Symbol 键不可枚举原型链
for...in✅❌❌✅
Object.keys✅❌❌❌
Object.getOwnPropertyNames✅❌✅❌
Object.getOwnPropertySymbols❌✅✅❌
Reflect.ownKeys✅✅✅❌

Reflect.ownKeys 是"自有属性"的完整集合,做深拷贝、代理拦截时常用它。

五、属性遍历顺序有讲究

现代 JS(ES2015 起)对字符串键有明确的遍历顺序规则,Object.keys、for...in 都遵守:

  1. 整数键(数组索引)优先,按数值升序。
  2. 其余字符串键,按插入顺序。
  3. Symbol 键,按插入顺序排在最后。
js
const obj = {}
obj.b = 1
obj[2] = 2
obj[1] = 3
obj.a = 4

Object.keys(obj) // ['1', '2', 'b', 'a']

理解这个顺序,能避免"为什么键的顺序和我写的不一样"的困惑。注意整数键的判定是 0 到 2^32-2 之间的非负整数。

六、转成数组再遍历(for...of)

如果就是喜欢 for...of 的写法,可以先转数组:

js
const obj = { a: 1, b: 2 }

for (const [key, value] of Object.entries(obj)) {
  console.log(key, value)
}

// 或先拿键
for (const key of Object.keys(obj)) {
  console.log(key, obj[key])
}

如果想让对象本身可迭代,可以自定义 Symbol.iterator:

js
const obj = {
  a: 1,
  b: 2,
  [Symbol.iterator]() {
    const entries = Object.entries(this)
    let index = 0
    return {
      next: () => ({
        value: entries[index],
        done: index++ >= entries.length,
      }),
    }
  },
}

for (const [k, v] of obj) {
  console.log(k, v)
}

七、深度遍历嵌套对象

上面都是浅遍历。要递归处理嵌套对象,写个递归函数:

js
function deepTraverse(obj, callback) {
  for (const key of Reflect.ownKeys(obj)) {
    const value = obj[key]
    callback(key, value, obj)

    if (value && typeof value === 'object') {
      deepTraverse(value, callback)
    }
  }
}

const data = { a: 1, b: { c: 2, d: { e: 3 } } }
deepTraverse(data, (key, value) => {
  console.log(key, '=>', typeof value === 'object' ? '[object]' : value)
})

真实场景记得用 WeakSet 记录已访问对象,避免循环引用导致栈溢出。

八、选型建议

需求推荐方法
遍历自身可枚举属性(最常见)Object.keys / values / entries
遍历时能接数组方法Object.entries(obj).forEach(...)
需要包含 Symbol / 不可枚举Reflect.ownKeys
需要继承链上的属性for...in + hasOwnProperty 过滤
只取值不要键Object.values
嵌套结构递归 + Reflect.ownKeys + WeakSet

小结

  • 对象不是可迭代对象,不能直接用 for...of,需先转数组。
  • 遍历方法差异在于三个维度:自有 / 继承、可枚举 / 不可枚举、字符串键 / Symbol 键。
  • Object.keys / values / entries 只涵盖自有可枚举字符串键,是日常首选。
  • Reflect.ownKeys 覆盖自有属性的全部,含 Symbol 和不可枚举。
  • for...in 会遍历原型链,务必用 Object.hasOwn 过滤。
  • 遍历顺序规则:整数键升序 → 字符串键插入序 → Symbol 插入序。
  • 深遍历要处理循环引用,避免栈溢出。
评论
0/100