for..in 和 Object.keys 的区别:从“遍历对象属性的坑”说起

在优化一个用户数据同步模块时,遇到一个诡异问题:

js 复制代码
// 用户配置对象
const userConfig = {
  name: 'Alice',
  age: 28,
  role: 'admin'
}

// 添加一个工具方法到原型
Object.prototype.export = function() {
  return JSON.stringify(this)
}

// 用 for..in 遍历配置
for (let key in userConfig) {
  console.log(key, userConfig[key])
}

输出结果让我吓一跳:

javascript 复制代码
name Alice
age 28
role admin
export function() { return JSON.stringify(this) }

"为什么多了一个 export? "------这正是 for..inObject.keys() 的核心差异所在。


一、问题场景:配置项遍历被"污染"

我们有个后台管理系统,需要将用户自定义配置导出为 CSV。原始代码如下:

js 复制代码
function exportConfigToCSV(config) {
  const headers = []
  const values = []

  for (let key in config) {
    headers.push(key)
    values.push(config[key])
  }

  return [headers.join(','), values.join(',')]
}

但上线后发现:导出的 CSV 多了一列 export,内容是函数代码

原因就是第三方库偷偷修改了 Object.prototype,而 for..in 会遍历所有可枚举属性,包括原型链上的。


二、解决方案:用 Object.keys 隔离原型污染

我们把遍历方式改成 Object.keys

js 复制代码
function exportConfigToCSV(config) {
  const headers = []
  const values = []

  // 🔍 Object.keys 只返回对象自身的可枚举属性
  Object.keys(config).forEach(key => {
    headers.push(key)
    values.push(config[key])
  })

  return [headers.join(','), values.join(',')]
}

现在输出正常了:

复制代码
name,age,role
Alice,28,admin

三、原理剖析:从表面到引擎底层的三层差异

1. 第一层:遍历范围不同(最核心区别)

for..in Object.keys()
遍历范围 自身 + 原型链上所有可枚举属性 仅对象自身的可枚举属性
是否包含继承属性 ✅ 是 ❌ 否

我们来验证一下:

js 复制代码
const parent = { x: 1 }
const child = Object.create(parent)
child.y = 2

console.log(Object.keys(child)) // ['y']
for (let key in child) console.log(key) // 'y', 'x'

🔍 关键点:for..in 会沿着 [[Prototype]] 链向上查找,直到 Object.prototype


2. 第二层:返回值类型与使用方式

for..in Object.keys()
返回类型 语法结构(不能赋值) 数组(可链式调用)
能否使用数组方法 ❌ 不能 ✅ 能(map/filter/forEach)
js 复制代码
// Object.keys 返回数组,可以轻松组合函数式编程
const doubled = Object.keys(obj)
  .filter(k => typeof obj[k] === 'number')
  .map(k => obj[k] * 2)

// for..in 必须配合外部变量
const doubled = []
for (let key in obj) {
  if (typeof obj[key] === 'number') {
    doubled.push(obj[key] * 2)
  }
}

3. 第三层:属性排序规则

for..in Object.keys()
属性顺序 ES2015 规范后与 Object.keys 一致 按照属性枚举顺序(数字键升序 + 其他键创建顺序)

虽然现代 JavaScript 引擎已经统一了顺序,但早期版本中 for..in 的顺序是不确定的 ,而 Object.keys 始终按可预测顺序返回。


四、设计哲学:为什么需要两种遍历方式?

for..in:为"动态对象探索"设计

适合场景:

  • 调试时查看对象完整结构
  • 需要处理继承属性的通用工具函数
  • 兼容老旧代码的属性探测
js 复制代码
// 工具函数:检查对象是否有任何有效数据
function hasData(obj) {
  for (let key in obj) {
    if (obj.hasOwnProperty(key)) return true
  }
  return false
}

Object.keys:为"数据处理"设计

适合场景:

  • 配置项遍历
  • 数组式操作(map/filter/reduce)
  • JSON 序列化准备
  • 防止原型污染的安全遍历
js 复制代码
// 安全地清理敏感字段
function sanitize(obj, blacklist) {
  return Object.keys(obj)
    .filter(key => !blacklist.includes(key))
    .reduce((acc, key) => {
      acc[key] = obj[key]
      return acc
    }, {})
}

五、对比主流遍历方案

方案 范围 返回类型 是否包含原型 适用场景
for..in 自身+原型 语法结构 动态探索、兼容性代码
Object.keys() 自身 数组 数据处理、安全遍历 ✅
Object.getOwnPropertyNames() 自身(含不可枚举) 数组 元编程、属性分析
Reflect.ownKeys() 自身(含 Symbol) 数组 代理拦截、完整反射

六、实战避坑指南

❌ 错误用法:for..in 不加 hasOwnProperty 判断

js 复制代码
for (let key in obj) {
  console.log(obj[key]) // 可能遍历到 Object.prototype 上的方法
}

✅ 正确做法:要么用 Object.keys,要么加判断

js 复制代码
// 方案1:推荐
Object.keys(obj).forEach(key => {
  console.log(obj[key])
})

// 方案2:传统写法
for (let key in obj) {
  if (obj.hasOwnProperty(key)) {
    console.log(obj[key])
  }
}

❌ 错误用法:误以为 for..in 适合数组遍历

js 复制代码
const arr = ['a', 'b', 'c']
arr.customProp = 'dontShowMe'

for (let i in arr) {
  console.log(arr[i]) // 'a', 'b', 'c', 'dontShowMe' ❌
}

✅ 正确做法:数组用 for..of 或 forEach

js 复制代码
// 数组遍历三剑客
for (let item of arr) { ... }
arr.forEach(item => { ... })
for (let i = 0; i < arr.length; i++) { ... }

七、举一反三:三个变体场景实现思路

  1. 需要遍历 Symbol 属性

    使用 Reflect.ownKeys(obj),它能同时返回字符串和 Symbol 键。

  2. 深度清理对象原型污染

    封装一个 safeKeys(obj) 函数,结合 Object.keysObject.prototype.toString.call 类型判断。

  3. 兼容性降级方案

    在不支持 Object.keys 的老浏览器中,用 for..in + hasOwnProperty 模拟实现。

js 复制代码
if (!Object.keys) {
  Object.keys = function(obj) {
    const keys = []
    for (let key in obj) {
      if (obj.hasOwnProperty(key)) {
        keys.push(key)
      }
    }
    return keys
  }
}

小结

for..inObject.keys() 的区别,本质是 "探索式遍历" vs "数据处理式遍历" 的哲学差异:

  • for..in 是"侦探"------它要查清对象所有的蛛丝马迹,包括祖辈遗传的属性
  • Object.keys() 是"会计"------它只关心当前对象账本上明确记录的条目

记住这个口诀:

要安全,用 keys;
要全面,用 for..in + hasOwn;
遍历数组?别乱来,for..of 才是真爱。

当你在写配置处理、数据导出、状态序列化时,请无脑选择 Object.keys()。只有当你明确需要检查继承属性时,才考虑 for..in

相关推荐
患得患失9493 分钟前
【前端】【vscode】【.vscode/settings.json】为单个项目配置自动格式化和开发环境
前端·vscode·json
飛_6 分钟前
解决VSCode无法加载Json架构问题
java·服务器·前端
YGY Webgis糕手之路3 小时前
OpenLayers 综合案例-轨迹回放
前端·经验分享·笔记·vue·web
90后的晨仔3 小时前
🚨XSS 攻击全解:什么是跨站脚本攻击?前端如何防御?
前端·vue.js
Ares-Wang3 小时前
JavaScript》》JS》 Var、Let、Const 大总结
开发语言·前端·javascript
90后的晨仔3 小时前
Vue 模板语法完全指南:从插值表达式到动态指令,彻底搞懂 Vue 模板语言
前端·vue.js
德育处主任3 小时前
p5.js 正方形square的基础用法
前端·数据可视化·canvas
烛阴3 小时前
Mix - Bilinear Interpolation
前端·webgl
90后的晨仔4 小时前
Vue 3 应用实例详解:从 createApp 到 mount,你真正掌握了吗?
前端·vue.js
德育处主任4 小时前
p5.js 矩形rect绘制教程
前端·数据可视化·canvas