在 JavaScript 中,对象是键值对的集合。与数组不同,对象本身不是一个内置的迭代器,这意味着你不能直接使用 `for…of` 循环像遍历数组元素那样遍历对象的属性。因此,我们需要特定的方法来访问对象中的每一个键或值。

为什么需要遍历对象?

遍历 JavaScript 对象是一个非常常见的操作,其目的是为了访问、检查或处理对象的每一个属性。具体场景包括:

  • 访问数据: 获取对象中存储的每一个值,例如处理从服务器接收到的配置数据或用户资料。
  • 处理属性: 对对象的每一个属性名或属性值进行某种操作,比如格式化、计算或验证。
  • 查找特定属性: 检查对象是否包含某个特定的属性名或属性值。
  • 复制或转换对象: 创建一个新对象,包含原对象的部分或全部属性,或者将对象的结构转换为另一种形式(如数组)。
  • 调试和检查: 查看一个对象包含了哪些属性及其对应的值,这在开发和调试时非常有用。

哪些部分可以遍历?

遍历对象时,你通常会关注以下几个方面:

  • 属性名 (Keys): 获取对象的所有属性名称。
  • 属性值 (Values): 获取对象的所有属性对应的值。
  • 属性条目 (Entries): 同时获取属性名和属性值组成的键值对。
  • 自有属性 (Own Properties): 对象直接拥有的属性,而不是从其原型链继承的属性。
  • 可枚举属性 (Enumerable Properties): 那些在遍历时被包含的属性(大多数常规属性都是可枚举的,但有些内置属性或通过特定方式定义的属性可能不是)。
  • 不可枚举属性 (Non-enumerable Properties): 那些默认情况下不参与标准遍历的属性。
  • Symbol 属性: 使用 Symbol 作为属性名定义的属性。

不同的遍历方法会侧重于这些不同类型的属性。

有多少种遍历对象的方法?如何选择?

JavaScript 提供了多种遍历对象的方法,每种方法有其特定的用途和行为。了解它们的区别是正确选择的关键。主要方法包括:

使用 `for…in` 循环

这是 JavaScript 中比较传统的遍历对象属性的方法。

它是如何工作的:

for...in 循环遍历对象所有可枚举的属性,包括对象自身的属性和从原型链上继承的可枚举属性。

遍历内容:

主要用于遍历对象的属性名 (Keys)

代码示例:


const obj = { a: 1, b: 2, c: 3 };
for (const key in obj) {
console.log(`属性名: ${key}, 属性值: ${obj[key]}`);
}

// 考虑原型链的情况
const protoObj = { protoProp: '我是原型属性' };
const inheritedObj = Object.create(protoObj);
inheritedObj.ownProp = '我是自有属性';

for (const key in inheritedObj) {
console.log(`for...in 发现: ${key}`); // 会输出 ownProp 和 protoProp
}

注意事项与如何处理:

for...in 的一个主要问题是它会遍历原型链上的属性。这在大多数情况下可能不是你想要的。为了只处理对象自身的属性,你需要结合使用 `Object.prototype.hasOwnProperty.call()` 方法进行判断。


for (const key in inheritedObj) {
if (Object.prototype.hasOwnProperty.call(inheritedObj, key)) {
console.log(`只遍历自有属性: ${key}, 值: ${inheritedObj[key]}`); // 只输出 ownProp
}
}

优点:

  • 语法简洁(如果不考虑 `hasOwnProperty`)。
  • 能够遍历原型链上的属性(如果需要的话)。

缺点:

  • 会遍历原型链上的属性,需要额外检查 (`hasOwnProperty`)。
  • 不保证属性的遍历顺序(尽管现代 JS 引擎对数字属性和字符串属性有一定规则,但不应完全依赖)。
  • 不遍历 Symbol 属性。

使用 `Object.keys()`

这是一个更现代、更安全的方法。

它是如何工作的:

Object.keys() 方法返回一个由给定对象自身可枚举字符串属性名组成的数组。

遍历内容:

返回一个包含所有自有可枚举字符串属性名数组

如何遍历:

因为它返回的是一个数组,你可以使用所有适用于数组的遍历方法来处理这个属性名数组,比如 `forEach` 或 `for…of`。

代码示例:


const obj = { a: 1, b: 2, c: 3 };
const keys = Object.keys(obj);
console.log(keys); // 输出: ['a', 'b', 'c']

// 使用 forEach 遍历
keys.forEach(key => {
console.log(`属性名: ${key}, 属性值: ${obj[key]}`);
});

// 使用 for...of 遍历
for (const key of keys) {
console.log(`属性名: ${key}, 属性值: ${obj[key]}`);
}

// 考虑原型链和不可枚举属性
const protoObj = { protoProp: '我是原型属性' };
const testObj = Object.create(protoObj, {
ownProp: { value: '我是自有属性', enumerable: true },
nonEnumerableProp: { value: '我是不可枚举属性', enumerable: false }
});

console.log(Object.keys(testObj)); // 输出: ['ownProp'] - 不包含 protoProp 和 nonEnumerableProp

优点:

  • 只遍历对象自身的属性,不会涉及原型链。
  • 返回一个数组,可以使用丰富的数组遍历方法。
  • 遍历顺序相对稳定(通常按照属性创建的顺序,但有特定规则)。

缺点:

  • 只遍历可枚举的属性。
  • 只遍历字符串属性名,不包含 Symbol 属性。

使用 `Object.values()`

类似于 `Object.keys()`,但关注属性值。

它是如何工作的:

Object.values() 方法返回一个由给定对象自身可枚举字符串属性值组成的数组。

遍历内容:

返回一个包含所有自有可枚举字符串属性值数组

如何遍历:

返回数组,使用 `forEach` 或 `for…of` 遍历。

代码示例:


const obj = { a: 1, b: 2, c: 3 };
const values = Object.values(obj);
console.log(values); // 输出: [1, 2, 3]

// 使用 forEach 遍历
values.forEach(value => {
console.log(`属性值: ${value}`);
});

// 使用 for...of 遍历
for (const value of values) {
console.log(`属性值: ${value}`);
}

// 考虑原型链和不可枚举属性
const protoObj = { protoProp: '我是原型属性' };
const testObj = Object.create(protoObj, {
ownProp: { value: '我是自有属性', enumerable: true },
nonEnumerableProp: { value: '我是不可枚举属性', enumerable: false }
});

console.log(Object.values(testObj)); // 输出: ['我是自有属性'] - 不包含原型值和不可枚举属性值

优点:

  • 直接获取所有属性值组成的数组,方便对值进行操作。
  • 只遍历对象自身的属性值。
  • 遍历顺序相对稳定。

缺点:

  • 只遍历可枚举属性的值。
  • 只遍历字符串属性名的值,不包含 Symbol 属性的值。
  • 无法直接获取对应的属性名。

使用 `Object.entries()`

这是同时需要键和值的现代方法。

它是如何工作的:

Object.entries() 方法返回一个由给定对象自身可枚举字符串属性的 `[key, value]` 对组成的数组。

遍历内容:

返回一个包含所有自有可枚举字符串属性的 `[属性名, 属性值]` 数组。

如何遍历:

返回数组,使用 `forEach` 或 `for…of` 遍历。特别是结合数组解构在 `for…of` 中使用非常方便。

代码示例:


const obj = { a: 1, b: 2, c: 3 };
const entries = Object.entries(obj);
console.log(entries); // 输出: [['a', 1], ['b', 2], ['c', 3]]

// 使用 forEach 遍历
entries.forEach(entry => {
console.log(`属性名: ${entry[0]}, 属性值: ${entry[1]}`);
});

// 使用 for...of 和解构遍历
for (const [key, value] of entries) {
console.log(`属性名: ${key}, 属性值: ${value}`);
}

// 考虑原型链和不可枚举属性
const protoObj = { protoProp: '我是原型属性' };
const testObj = Object.create(protoObj, {
ownProp: { value: '我是自有属性', enumerable: true },
nonEnumerableProp: { value: '我是不可枚举属性', enumerable: false }
});

console.log(Object.entries(testObj)); // 输出: [['ownProp', '我是自有属性']] - 不包含原型属性和不可枚举属性

优点:

  • 同时获取属性名和属性值,非常方便。
  • 只遍历对象自身的属性。
  • 返回一个数组,可以使用数组方法。
  • 遍历顺序相对稳定。

缺点:

  • 只遍历可枚举的属性。
  • 只遍历字符串属性名,不包含 Symbol 属性。

遍历不可枚举属性和 Symbol 属性

如果你需要遍历对象的自有属性,包括那些不可枚举的属性或者 Symbol 属性,可以使用以下方法:

使用 `Object.getOwnPropertyNames()`

返回一个由给定对象自身所有字符串属性名(无论是否可枚举)组成的数组。


const testObj = Object.create({}, {
ownProp: { value: '我是自有属性', enumerable: true },
nonEnumerableProp: { value: '我是不可枚举属性', enumerable: false }
});

console.log(Object.getOwnPropertyNames(testObj)); // 输出: ['ownProp', 'nonEnumerableProp']

使用 `Object.getOwnPropertySymbols()`

返回一个由给定对象自身所有 Symbol 属性名组成的数组。


const sym1 = Symbol('sym1');
const sym2 = Symbol('sym2');
const objWithSymbols = {
a: 1,
[sym1]: 'symbol value 1',
[sym2]: 'symbol value 2'
};

console.log(Object.getOwnPropertySymbols(objWithSymbols)); // 输出: [Symbol(sym1), Symbol(sym2)]

使用 `Reflect.ownKeys()`

这是最全面的方法,返回一个由给定对象自身所有属性名(包括字符串和 Symbol,无论是否可枚举)组成的数组。


const sym1 = Symbol('sym1');
const objComprehensive = Object.create({}, {
a: { value: 1, enumerable: true },
b: { value: 2, enumerable: false },
[sym1]: { value: 'symbol value', enumerable: true }
});

console.log(Reflect.ownKeys(objComprehensive)); // 输出: ['a', 'b', Symbol(sym1)]

使用 `Reflect.ownKeys()` 配合 `for…of` 循环,可以遍历到对象的所有自有属性。

哪里会用到对象遍历?

对象遍历无处不在。一些具体的应用场景:

  • 数据处理: 从 API 获取 JSON 数据后,通常需要遍历其中的对象来提取或转换信息。
  • 配置文件: 读取或修改配置对象中的每一项设置。
  • 表单处理: 收集表单输入,将它们组织成一个对象,然后遍历这个对象进行验证或提交。
  • 组件属性 (Props): 在前端框架中,组件接收的属性 often 是一个对象,需要遍历来应用到组件内部。
  • 对象深拷贝: 实现一个函数来递归地复制对象的所有属性。
  • 路由配置: Web 应用的路由规则可能存储在一个对象中,需要遍历来匹配当前 URL。

如何选择合适的遍历方法?

选择哪种方法取决于你的具体需求:

  1. 只需要遍历自有、可枚举的字符串属性名? 使用 `Object.keys()`,然后配合 `forEach` 或 `for…of`。
  2. 只需要遍历自有、可枚举的字符串属性值? 使用 `Object.values()`,然后配合 `forEach` 或 `for…of`。
  3. 需要同时遍历自有、可枚举的字符串属性名和属性值? 使用 `Object.entries()`,然后配合 `for…of` 和解构。
  4. 需要遍历所有自有字符串属性(包括不可枚举的)? 使用 `Object.getOwnPropertyNames()`。
  5. 需要遍历所有自有 Symbol 属性? 使用 `Object.getOwnPropertySymbols()`。
  6. 需要遍历所有自有属性(包括字符串和 Symbol,可枚举或不可枚举)? 使用 `Reflect.ownKeys()`。
  7. 需要在不支持现代方法的旧环境运行,或者需要遍历原型链上的可枚举属性? 使用 `for…in`,并强烈建议使用 `hasOwnProperty` 进行过滤以避免原型属性干扰。

对于大多数现代 Web 应用开发,Object.keys(), Object.values(), 和 Object.entries() 是处理对象自有可枚举属性的首选方法,它们返回数组,使用起来更灵活方便。

遍历时的注意事项

  • 属性顺序: 尽管现代 JavaScript 引擎倾向于按照一定规则(如数字键在前,字符串键按插入顺序等)排列可枚举的自有字符串属性,但 `for…in` 的顺序规范上是不保证的。 `Object.keys/values/entries` 及其派生方法的顺序通常是稳定的,但也不应在对顺序有严格要求的场景下过度依赖,除非规范明确保证。
  • 在遍历过程中修改对象: 在遍历一个对象的同时添加、删除或修改属性可能会导致不可预测的行为。如果需要在遍历过程中修改对象,最好先将属性收集到一个临时的数据结构中(如数组),然后在遍历完成后再对原对象进行修改,或者遍历一个对象的副本。
  • 性能: 对于绝大多数应用场景,不同遍历方法的性能差异微乎其微,不应作为首要的考虑因素。优先选择可读性强、符合需求的现代方法。

掌握这些对象遍历方法,是有效操作 JavaScript 数据的基本功。

js对象遍历

By admin

发表回复