一、遍历对象:从传统到现代

遍历对象是日常开发中常见的操作,比如你需要检查一个用户对象的所有属性,或者从配置对象中提取特定信息。JavaScript 提供了多种遍历方式,每种方式适合不同场景,我们来逐一拆解。

1. for...in 循环:老牌但需谨慎

for...in 是最传统的遍历方式,它会遍历对象自身及其原型链上的所有可枚举属性。

const person = {
  name: '张三',
  age: 30
}

for (const key in person) {
  console.log(`${key}: ${person[key]}`)
}
// 输出:
// name: 张三
// age: 30

注意for...in 会遍历原型链上的属性,这可能导致意外的结果。为了只处理对象自身的属性,搭配 hasOwnProperty 是个好习惯:

for (const key in person) {
  if (person.hasOwnProperty(key)) {
    console.log(`${key}: ${person[key]}`)
  }
}

适用场景:当你需要遍历对象属性并可能关心原型链上的属性时,for...in 是不错的选择。但现代开发中,推荐更简洁的替代方案。


2. Object.keys():获取键名数组,现代首选

Object.keys() 返回对象自身可枚举属性的键名数组,结合数组方法(如 forEach)使用,代码更简洁,且无需担心原型链问题。

const person = {
  name: '张三',
  age: 30
}

const keys = Object.keys(person)
keys.forEach(key => {
  console.log(`${key}: ${person[key]}`)
})

优点

  • 只返回对象自身的可枚举属性,安全可靠。
  • 返回数组后,可以灵活使用 mapfilter 等数组方法。

适用场景:需要遍历键名,或者基于键名做进一步处理时,Object.keys() 是最常用的方法。


3. Object.values():直接获取值

如果只关心对象的值,Object.values() 是最佳选择。它返回对象自身可枚举属性的值数组。

const person = {
  name: '张三',
  age: 30
}

const values = Object.values(person)
console.log(values) // ['张三', 30]

适用场景:当你只需要处理值的集合,比如统计、转换等操作。


4. Object.entries():键值对齐飞

Object.entries() 返回一个键值对的数组,每个元素是 [key, value] 形式。搭配 for...of 和解构赋值,代码优雅又直观。

const person = {
  name: '张三',
  age: 30
}

for (const [key, value] of Object.entries(person)) {
  console.log(`${key}: ${value}`)
}

优点

  • 同时获取键和值,适合需要一起操作的场景。
  • 结合解构赋值,代码简洁且可读性强。

适用场景:需要同时处理键和值的场景,比如格式化输出或转换对象结构。


5. 特殊场景:不可枚举属性和 Symbol

如果需要遍历不可枚举属性或 Symbol 属性,可以用以下方法:

  • Object.getOwnPropertyNames():获取所有自身属性(包括不可枚举,但不包括 Symbol)。
  • Object.getOwnPropertySymbols():获取所有 Symbol 属性。
  • Reflect.ownKeys():获取所有自身属性(包括不可枚举和 Symbol)。
const person = {
  name: '张三',
  [Symbol('id')]: 123
}
Object.defineProperty(person, 'age', {
  value: 30,
  enumerable: false
})

console.log(Reflect.ownKeys(person)) // ['name', 'age', Symbol(id)]

适用场景:处理复杂对象或需要完全控制属性遍历时。


遍历方法选择建议

根据场景选择合适的遍历方式:

  • 只遍历自身可枚举属性:用 Object.keys(),简单高效。
  • 需要键值对:用 Object.entries() 搭配 for...of,优雅简洁。
  • 只关心值:用 Object.values(),直接获取值数组。
  • 需要原型链属性:用 for...in 配合 hasOwnProperty
  • 特殊属性(如不可枚举或 Symbol):用 Reflect.ownKeys()

二、判断对象是否为空:简单又实用

在开发中,经常需要检查一个对象是否为空(即没有自身可枚举属性)。以下是几种常用方法,结合优缺点分析。

1. Object.keys():最推荐

通过检查键数组的长度,简单直观。

const obj = {}
if (Object.keys(obj).length === 0) {
  console.log('对象是空的')
}

优点:清晰、性能好,现代开发首选。


2. Object.entries() 或 Object.values()

Object.keys() 类似,检查返回数组的长度。

const obj = {}
if (Object.entries(obj).length === 0) {
  console.log('对象是空的')
}
// 或
if (Object.values(obj).length === 0) {
  console.log('对象是空的')
}

适用场景:如果你已经在用 Object.entries()Object.values() 处理其他逻辑,可以顺手用它们判断。


3. for...in 循环:传统但可靠

通过遍历检查是否有属性,适合需要兼容旧代码的场景。

const obj = {}
function isEmpty(obj) {
  for (const key in obj) {
    if (Object.prototype.hasOwnProperty.call(obj, key)) {
      return false
    }
  }
  return true
}

if (isEmpty(obj)) {
  console.log('对象是空的')
}

缺点:代码稍长,性能略低于 Object.keys()


4. JSON.stringify():简单但有局限

将对象转为 JSON 字符串,检查是否为 '{}'

const obj = {}
if (JSON.stringify(obj) === '{}') {
  console.log('对象是空的')
}

注意

  • 无法处理 Symbol 或不可枚举属性。
  • 性能开销较大,不适合频繁调用。

5. 第三方库(如 Lodash)

如果项目中已使用 Lodash,可以直接用 _.isEmpty()

if (_.isEmpty(obj)) {
  console.log('对象是空的')
}

适用场景:项目已引入 Lodash,且追求代码简洁。


注意事项

  • 以上方法只检查自身可枚举属性,原型链属性不会影响结果。
  • 始终确保对象不是 nullundefined,否则可能抛出错误。
  • 推荐优先使用 Object.keys(),因为它简单、性能好且直观。

三、检查对象是否有某个属性

在开发中,经常需要判断对象是否包含某个属性。JavaScript 提供了多种方法,适合不同场景。

1. in 操作符:检查自有和原型链属性

in 操作符会检查对象自身及其原型链上是否有指定属性。

const person = { name: '张三', age: 30 }
if ('name' in person) {
  console.log('有 name 属性')
}

适用场景:需要考虑原型链的场景。


2. hasOwnProperty():只检查自有属性

hasOwnProperty() 只检查对象自身的属性,忽略原型链。

const person = { name: '张三', age: 30 }
if (person.hasOwnProperty('name')) {
  console.log('有自己的 name 属性')
}

注意:如果对象自身覆盖了 hasOwnProperty,可能导致问题。


3. Object.hasOwn():现代推荐(ES2022)

Object.hasOwn()hasOwnProperty() 的现代替代,语法更简洁且更安全。

const person = { name: '张三', age: 30 }
if (Object.hasOwn(person, 'name')) {
  console.log('有自己的 name 属性')
}

优点:不会被对象自身覆盖,推荐在现代项目中使用。


4. 直接访问判断:简单但有风险

通过检查属性是否为 undefined 来判断是否存在。

const person = { name: '张三', age: 30 }
if (person.name !== undefined) {
  console.log('name 属性存在且不为 undefined')
}

风险:如果属性值本身是 undefinednull 或其他假值,会误判。


5. 可选链操作符 ?.:安全访问嵌套属性

可选链操作符适合处理可能不存在的深层嵌套属性。

const person = { name: '张三', profile: { hobby: '读书' } }
const hobby = person.profile?.hobby
console.log(hobby) // '读书'

适用场景:访问嵌套属性时防止 undefined 错误。


属性检查选择建议

  • 只检查自有属性:用 Object.hasOwn()(现代)或 hasOwnProperty()
  • 需要考虑原型链:用 in 操作符。
  • 处理嵌套属性:用可选链 ?.
  • 追求简洁但需小心误判:直接访问属性(谨慎使用)。

四、for...in vs for...of:别再混淆

for...infor...of 是两个容易混淆的循环,搞清楚它们的区别对正确操作对象和集合至关重要。

1. for...in:遍历对象键名

for...in 专门用来遍历对象的可枚举属性,包括原型链上的属性。

const person = { name: '张三', age: 30 }
for (const key in person) {
  console.log(key, person[key])
}
// 输出:
// name 张三
// age 30

注意:需要用 hasOwnProperty 过滤原型链属性。


2. for...of:遍历集合元素值

for...of 用于遍历可迭代对象(如数组、字符串、Map、Set)的元素值。

const fruits = ['苹果', '香蕉', '橙子']
for (const fruit of fruits) {
  console.log(fruit)
}
// 输出:
// 苹果
// 香蕉
// 橙子

注意:普通对象不可直接用 for...of,除非自定义迭代器。


3. 混合示例:更直观的对比

来看一个数组的例子:

const colors = ['红', '绿', '蓝']
colors.customProperty = '自定义属性'

for (const prop in colors) {
  console.log(prop, colors[prop])
}
// 输出:
// 0 红
// 1 绿
// 2 蓝
// customProperty 自定义属性

for (const value of colors) {
  console.log(value)
}
// 输出:
// 红
// 绿
// 蓝

总结

  • for...in:遍历键名,适合对象。
  • for...of:遍历值,适合数组、Set 等可迭代对象。
  • 口诀:对象属性用 for...in,集合元素用 for...of

五、Object.freeze():保护对象不被修改

在开发中,防止对象被意外修改是确保代码健壮的重要一环。Object.freeze() 能让对象进入“只读”状态,特别适合需要不变性的场景。

1. 基本用法

冻结对象后,无法修改、添加或删除属性。

const user = {
  name: '张三',
  age: 30
}

Object.freeze(user)

user.age = 31       // 无效
user.city = '北京'   // 无效
delete user.age     // 无效

特点

  • 禁止添加、修改、删除属性。
  • 锁定属性特性(如可枚举性)和原型。

2. 注意事项

  • 浅冻结:只冻结对象自身,嵌套对象不受影响。
const company = {
  name: '科技公司',
  location: {
    city: '上海'
  }
}

Object.freeze(company)
company.location.city = '北京' // 有效!嵌套对象未冻结
  • 深度冻结:需要递归冻结嵌套对象。
function deepFreeze(obj) {
  Object.keys(obj).forEach(key => {
    if (typeof obj[key] === 'object' && obj[key] !== null) {
      deepFreeze(obj[key])
    }
  })
  return Object.freeze(obj)
}
  • 严格模式:尝试修改冻结对象会抛出错误,非严格模式下默默失败。
  • 检查冻结状态:用 Object.isFrozen(obj) 判断。

3. 实际应用场景

  • 常量对象:创建不可修改的配置对象。
  • 数据不变性:在函数式编程中确保数据一致性。
  • 性能优化:冻结对象可能触发引擎优化。

六、从第一性原理看对象操作

回到第一性原理,对象操作的核心是理解数据的本质和操作需求。对象本质是一个键值对集合,遍历、检查、保护的每种方法都在解决特定问题:

  • 遍历:是为了访问键、值或两者,方法的选择取决于是否需要原型链、不可枚举属性或特定的遍历方式。
  • 检查空对象:核心是判断是否有自身可枚举属性,Object.keys() 是最直接的解法。
  • 属性检查:区分自有属性和原型链属性,现代 Object.hasOwn() 是更安全的选择。
  • 冻结:为了保护数据不变性,Object.freeze() 提供了简单高效的方案,但要注意嵌套对象的处理。

通过拆解这些需求的本质,我们可以更清晰地选择合适工具,而不是机械地套用方法。


七、总结与最佳实践

掌握 JavaScript 对象操作的关键在于理解场景和工具的适用性。以下是几条实用建议:

  1. 遍历对象:优先用 Object.keys()Object.entries()Object.values(),它们现代且安全。需要原型链时用 for...in,特殊场景用 Reflect.ownKeys()
  2. 判断空对象Object.keys().length === 0 是最简洁可靠的方法。
  3. 检查属性:用 Object.hasOwn()(ES2022)或 in 操作符,视是否需要原型链而定。
  4. for...in vs for...of:对象用 for...in,可迭代集合用 for...of
  5. 保护对象:用 Object.freeze() 确保不变性,嵌套对象需要 deepFreeze