MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

JavaScript可迭代对象的遍历方式

2022-01-164.1k 阅读

JavaScript 可迭代对象的遍历方式

在 JavaScript 的世界里,可迭代对象(iterable object)是一种拥有迭代器(iterator)的对象,它允许我们按照顺序逐个访问对象中的元素。许多内置的 JavaScript 数据结构,如数组(Array)、字符串(String)、集合(Set)和映射(Map)等,都是可迭代对象。遍历这些可迭代对象是编程中非常常见的操作,JavaScript 提供了多种方式来实现这一目的。下面我们将深入探讨各种遍历方式的本质和用法,并通过代码示例来加深理解。

for...of 循环

for...of 循环是 ES6 引入的一种简洁且强大的遍历可迭代对象的方式。它直接遍历可迭代对象的元素,而不是像传统的 for 循环那样通过索引来访问。

基本语法

for (let value of iterable) {
    // 处理 value
}

这里的 iterable 是一个可迭代对象,value 是每次迭代中从可迭代对象获取的元素值。

遍历数组

const numbers = [1, 2, 3, 4, 5];
for (let number of numbers) {
    console.log(number);
}

在这个例子中,for...of 循环依次从 numbers 数组中取出每个元素并打印。

遍历字符串

const str = "hello";
for (let char of str) {
    console.log(char);
}

字符串在 JavaScript 中也是可迭代对象,for...of 循环会逐个输出字符串中的字符。

遍历 Set

const mySet = new Set([1, 2, 2, 3]);
for (let value of mySet) {
    console.log(value);
}

Set 是一种不包含重复值的集合,for...of 循环会按插入顺序遍历 Set 中的所有值。

遍历 Map

const myMap = new Map([
    ["name", "John"],
    ["age", 30]
]);
for (let [key, value] of myMap) {
    console.log(`${key}: ${value}`);
}

Map 是一种键值对的集合,for...of 循环在遍历 Map 时,可以通过解构赋值同时获取键和值。

本质原理

for...of 循环的本质是利用可迭代对象的迭代器协议。当一个对象实现了 Symbol.iterator 方法时,它就是可迭代的。Symbol.iterator 方法返回一个迭代器对象,这个迭代器对象必须有一个 next() 方法。每次调用 next() 方法,迭代器会返回一个包含 valuedone 属性的对象。value 是当前迭代的值,done 是一个布尔值,表示是否已经迭代完所有元素。for...of 循环会不断调用 next() 方法,直到 donetrue 为止。

for...in 循环

for...in 循环主要用于遍历对象的可枚举属性。虽然它也可以用于数组,但它与 for...of 循环有本质的区别。

基本语法

for (let key in object) {
    // 处理 key
}

这里的 object 是要遍历的对象,key 是对象的属性名。

遍历对象

const person = {
    name: "John",
    age: 30,
    city: "New York"
};
for (let prop in person) {
    console.log(`${prop}: ${person[prop]}`);
}

在这个例子中,for...in 循环会遍历 person 对象的所有可枚举属性,并输出属性名和属性值。

遍历数组

const numbers = [1, 2, 3, 4, 5];
for (let index in numbers) {
    console.log(numbers[index]);
}

虽然可以用 for...in 遍历数组,但它遍历的是数组的索引(也就是属性名),而不是数组元素本身。而且,由于对象的属性不一定是按照顺序存储的,所以在遍历数组时,for...in 可能不会按照数组元素的实际顺序进行遍历。

本质原理

for...in 循环遍历对象的属性是基于对象的原型链。它会从对象本身开始,然后沿着原型链向上查找所有可枚举的属性。这意味着如果对象的原型上有可枚举的属性,也会被遍历到。例如:

const myArray = [1, 2, 3];
Array.prototype.customProp = "This is a custom property";
for (let prop in myArray) {
    console.log(prop);
}

在这个例子中,除了数组的索引 012 外,customProp 也会被遍历出来,这在很多情况下并不是我们想要的结果。所以,一般情况下,遍历数组应该优先使用 for...of 循环,而遍历对象的属性则使用 for...in 循环。

Array.prototype.forEach()

forEach() 方法是数组特有的方法,用于对数组的每个元素执行一次给定的函数。

基本语法

array.forEach(function (element, index, array) {
    // 处理 element
}, thisArg);

这里的 array 是要遍历的数组,element 是当前正在处理的数组元素,index 是元素的索引,array 是调用 forEach() 的数组本身。thisArg 是可选参数,用于指定回调函数内部 this 的值。

示例

const numbers = [1, 2, 3, 4, 5];
numbers.forEach(function (number, index) {
    console.log(`Index ${index}: ${number}`);
});

在这个例子中,forEach() 方法会对 numbers 数组的每个元素调用回调函数,打印出元素的索引和值。

本质原理

forEach() 方法内部是通过迭代数组的索引来依次访问每个元素的。它会从索引 0 开始,直到索引小于数组的长度为止。每次访问一个元素时,就会调用传入的回调函数,并将当前元素、索引和数组本身作为参数传递给回调函数。需要注意的是,forEach() 方法不会改变原数组,而且它不能通过 breakreturn 语句中断循环,只能通过抛出异常来停止。

使用迭代器手动遍历

除了上述几种便捷的遍历方式外,我们还可以手动使用迭代器来遍历可迭代对象。

获取迭代器

要手动使用迭代器,首先需要获取可迭代对象的迭代器。这可以通过调用对象的 Symbol.iterator 方法来实现。

const numbers = [1, 2, 3];
const iterator = numbers[Symbol.iterator]();

这里的 iterator 就是 numbers 数组的迭代器。

使用 next() 方法

迭代器有一个 next() 方法,每次调用它都会返回一个包含 valuedone 属性的对象。

let result = iterator.next();
while (!result.done) {
    console.log(result.value);
    result = iterator.next();
}

在这个例子中,我们通过不断调用 next() 方法,获取数组的每个元素并打印,直到 done 属性为 true,表示已经遍历完所有元素。

本质原理

这种手动遍历方式直接利用了迭代器协议。迭代器对象通过维护一个内部状态来记录当前遍历的位置,每次调用 next() 方法时,它会根据这个状态返回下一个值,并更新状态。这种方式虽然比较底层,但对于理解可迭代对象和迭代器的工作原理非常有帮助。

使用展开运算符(...)遍历

展开运算符(...)在 JavaScript 中有着多种用途,其中之一就是可以用于遍历可迭代对象。

基本用法

const numbers = [1, 2, 3];
const newArray = [...numbers];
console.log(newArray);

这里通过展开运算符将 numbers 数组的元素展开并创建了一个新的数组。

遍历并操作

展开运算符不仅可以用于创建新数组,还可以在遍历的同时对元素进行操作。

const numbers = [1, 2, 3];
const squaredNumbers = [...numbers.map(num => num * num)];
console.log(squaredNumbers);

在这个例子中,先使用 map() 方法对 numbers 数组的每个元素进行平方操作,然后通过展开运算符将结果收集到一个新数组中。

本质原理

展开运算符在遍历可迭代对象时,实际上是在内部使用了迭代器。它通过不断调用迭代器的 next() 方法来获取可迭代对象的每个元素,并将这些元素展开到相应的位置。例如在创建新数组时,它会将可迭代对象的元素逐个添加到新数组中。

使用 reduce() 方法遍历

reduce() 方法是数组的一个强大方法,它可以对数组中的所有元素执行一个累加器函数,从而将数组“化简”为一个单一的值。虽然它的主要目的不是遍历,但在实现过程中也涉及到对数组元素的逐个访问。

基本语法

array.reduce(function (accumulator, currentValue, currentIndex, array) {
    // 返回累加结果
}, initialValue);

这里的 array 是要操作的数组,accumulator 是累加器的初始值或上一次调用回调函数的返回值,currentValue 是当前正在处理的数组元素,currentIndex 是元素的索引,array 是调用 reduce() 的数组本身。initialValue 是可选参数,用于指定累加器的初始值。

示例

const numbers = [1, 2, 3, 4, 5];
const sum = numbers.reduce(function (acc, num) {
    return acc + num;
}, 0);
console.log(sum);

在这个例子中,reduce() 方法从初始值 0 开始,依次将数组中的每个元素与累加器 acc 相加,最终得到数组元素的总和。

本质原理

reduce() 方法通过迭代数组的索引,从索引 0 开始(如果提供了 initialValue,则从索引 0 开始;否则从索引 1 开始,将 initialValue 设为数组的第一个元素),每次调用回调函数并传递当前的累加器值、当前元素、当前索引和数组本身。回调函数的返回值会作为下一次调用的累加器值,直到遍历完所有元素,最终返回累加的结果。

各种遍历方式的比较与选择

  1. for...of 与 for...in
    • for...of 主要用于遍历可迭代对象的元素,适用于数组、字符串、Set 和 Map 等。它直接访问元素值,并且按照元素的顺序进行遍历。
    • for...in 主要用于遍历对象的可枚举属性,包括对象本身和原型链上的可枚举属性。在遍历数组时,它遍历的是索引(属性名),可能导致顺序不一致,且会包含原型上的属性,所以一般不用于遍历数组。
  2. for...of 与 forEach()
    • for...of 是一个循环结构,可以使用 breakcontinue 等语句来控制循环流程,更加灵活。
    • forEach() 是数组的方法,它不能中断循环,只能通过抛出异常停止。forEach() 适用于对数组每个元素执行简单的操作,并且不需要控制循环流程的场景。
  3. 手动迭代与其他方式
    • 手动使用迭代器遍历虽然底层且复杂,但能让我们深入理解可迭代对象和迭代器的工作原理,适用于需要精确控制迭代过程的特殊场景。
    • 其他遍历方式如 for...of、forEach() 等则更加便捷和常用,适用于大多数普通的遍历需求。
  4. 展开运算符与其他方式
    • 展开运算符主要用于将可迭代对象展开到其他数据结构中,如创建新数组或传递函数参数等。它不是传统意义上的遍历方式,但在展开过程中涉及到对可迭代对象的遍历。
    • 与其他遍历方式相比,展开运算符更侧重于数据的收集和重组,而不是对每个元素进行详细的操作。
  5. reduce() 与其他方式
    • reduce() 的主要目的是将数组化简为一个单一的值,通过累加器函数对数组元素进行累积操作。
    • 与其他遍历方式不同,reduce() 更注重结果的计算,而不是单纯的遍历和操作每个元素。如果只是需要遍历并操作元素,使用 for...of 或 forEach() 可能更合适;如果需要对数组进行累积计算,reduce() 则是更好的选择。

在实际编程中,我们需要根据具体的需求和场景来选择合适的遍历方式。理解每种遍历方式的本质和特点,能够帮助我们编写出更加高效、简洁和可读的代码。

综上所述,JavaScript 提供了丰富多样的可迭代对象遍历方式,每种方式都有其独特的用途和适用场景。无论是简单的数组遍历,还是复杂的对象属性操作,我们都能找到合适的方法来完成任务。通过深入理解这些遍历方式的本质和区别,我们可以更加灵活地运用它们,提升代码的质量和效率。希望通过本文的介绍,读者对 JavaScript 可迭代对象的遍历方式有了更全面和深入的认识。