在 JavaScript 中处理集合数据(如数组、字符串、Map、Set 等)时,我们经常需要遍历它们的元素。ES6 引入的迭代器 (Iterators) 协议和可迭代对象 (Iterables) 协议提供了一种统一、标准化的方式来访问任何集合的元素,无论其底层数据结构如何。这个强大的机制是现代 JavaScript 的基石,它使得 for...of 循环、展开运算符 (...) 以及解构赋值等特性成为可能,并为我们自定义数据结构的遍历行为打开了大门。

迭代器不仅仅是一种循环方式的替代品,它更是实现惰性求值 (Lazy Evaluation) 的关键。这意味着数据可以按需生成,而不是一次性全部加载到内存中,这对于处理大型数据集、无限序列或节省资源的应用场景至关重要。

本教程将全面深入地探讨 JavaScript 中迭代器和可迭代对象的方方面面。我们将从解释可迭代协议迭代器协议的核心定义开始,详细剖析它们各自的组成部分。随后,我们将通过丰富的示例,展示 JavaScript 内置的可迭代对象如何与 for...of 循环无缝协作。本教程的重点将是指导您如何创建自定义的可迭代对象,从而为您的定制数据结构赋予遍历能力。此外,我们还将深入学习 生成器 (Generators) 函数,它们是如何以更简洁、更优雅的方式构建迭代器的。最后,我们将讨论迭代器的主要优势(如统一接口、惰性求值)以及使用注意事项最佳实践,帮助您彻底掌握迭代器的强大功能,编写出更灵活、更高效的 JavaScript 代码。


1. 迭代器 (Iterators) 与 可迭代对象 (Iterables):核心概念

要理解 JavaScript 的迭代机制,首先要区分两个紧密相关的协议:可迭代协议 (Iterable Protocol)迭代器协议 (Iterator Protocol)

1.1 可迭代协议 (Iterable Protocol)

一个对象如果实现了可迭代协议,那么它就是一个可迭代对象 (Iterable)
可迭代协议的核心在于:该对象(或其原型链上的某个对象)必须有一个键为 Symbol.iterator 的方法。 这个方法被称为迭代器工厂函数,它不接受任何参数,并且必须返回一个符合迭代器协议的对象。

常见的内置可迭代对象包括:

  • Array
  • String
  • Map
  • Set
  • NodeList (DOM 集合)
  • arguments 对象
  • TypedArray
  • Generator 返回的对象 (Generator Object)

如何判断一个对象是否可迭代?
你可以检查它是否拥有 Symbol.iterator 方法:

const arr = [1, 2, 3];
console.log(typeof arr[Symbol.iterator]); // 输出: "function" (数组是可迭代的)

const str = "hello";
console.log(typeof str[Symbol.iterator]); // 输出: "function" (字符串是可迭代的)

const obj = { a: 1, b: 2 };
console.log(typeof obj[Symbol.iterator]); // 输出: "undefined" (普通对象默认不可迭代)

动手实践: 在浏览器控制台中尝试 typeof new Map()[Symbol.iterator]typeof new Set()[Symbol.iterator],验证它们也是可迭代的。

1.2 迭代器协议 (Iterator Protocol)

一个对象如果实现了迭代器协议,那么它就是一个迭代器 (Iterator)
迭代器协议的核心在于:该对象必须有一个 next() 方法。 next() 方法不接受任何参数,并且每次调用时都必须返回一个对象,这个对象包含两个属性:

  • value 表示迭代器序列中的下一个值。
  • done 一个布尔值,表示迭代是否完成。
    • false:表示迭代尚未完成,value 属性包含一个有效值。
    • true:表示迭代已完成,value 属性通常是 undefined(尽管也可以是任何值,但通常建议省略或为 undefined)。

迭代器的生命周期:

  1. 调用可迭代对象的 [Symbol.iterator]() 方法,得到一个迭代器。
  2. 重复调用迭代器的 next() 方法。
  3. 每次 next() 调用返回 { value: ..., done: false },直到迭代完成。
  4. next() 调用返回 { value: ..., done: true } 时,迭代结束。

手动使用迭代器:

const myArray = ['a', 'b', 'c'];
const myIterator = myArray[Symbol.iterator](); // 获取数组的迭代器

console.log(myIterator.next()); // { value: 'a', done: false }
console.log(myIterator.next()); // { value: 'b', done: false }
console.log(myIterator.next()); // { value: 'c', done: false }
console.log(myIterator.next()); // { value: undefined, done: true } (迭代完成)
console.log(myIterator.next()); // { value: undefined, done: true } (之后仍返回 true)

2. for...of 循环:遍历可迭代对象

for...of 循环是 ES6 引入的,专门用于遍历可迭代对象的语法。它在内部会自动调用可迭代对象的 [Symbol.iterator]() 方法来获取迭代器,然后反复调用迭代器的 next() 方法,直到 done 属性为 true

for...of 的优势:

  • 简洁明了: 语法比传统的 for 循环更简洁。
  • 直接获取值: 每次迭代直接得到元素的值,而不是索引。
  • 适用于所有可迭代对象: 统一的遍历接口。

示例:for...of 遍历内置可迭代对象

// 遍历数组
const numbers = [10, 20, 30];
for (const num of numbers) {
    console.log(`Array element: ${num}`);
}
// 输出:
// Array element: 10
// Array element: 20
// Array element: 30

// 遍历字符串
const greeting = "Hello";
for (const char of greeting) {
    console.log(`String character: ${char}`);
}
// 输出:
// String character: H
// String character: e
// String character: l
// String character: l
// String character: o

// 遍历 Map (键值对)
const myMap = new Map([['name', 'Alice'], ['age', 30]]);
for (const [key, value] of myMap) { // 使用解构赋值
    console.log(`Map entry: ${key} = ${value}`);
}
// 输出:
// Map entry: name = Alice
// Map entry: age = 30

// 遍历 Set
const mySet = new Set([1, 2, 2, 3]);
for (const item of mySet) {
    console.log(`Set item: ${item}`);
}
// 输出:
// Set item: 1
// Set item: 2
// Set item: 3

for...in vs for...of

  • for...in 主要用于遍历对象的可枚举属性名(字符串键),会遍历到原型链上的属性。不推荐用于数组遍历,因为它可能遍历到非数字索引或原型属性。
  • for...of 遍历可迭代对象的值。只适用于实现了 Symbol.iterator 协议的对象。

3. 创建自定义可迭代对象

我们可以通过手动实现可迭代协议来让自定义的数据结构支持 for...of 循环。

示例:实现一个自定义的 Range 对象

我们来创建一个 Range 对象,它能表示一个数字范围,并可以像数组一样被遍历。

class MyRange {
    constructor(start, end) {
        this.start = start;
        this.end = end;
    }

    // 实现可迭代协议:必须有一个 Symbol.iterator 方法
    [Symbol.iterator]() {
        let current = this.start; // 初始化当前的迭代值
        const end = this.end;     // 缓存结束值

        return { // 返回一个迭代器对象 (必须包含 next 方法)
            next() {
                if (current <= end) {
                    return { value: current++, done: false }; // 返回当前值,并递增
                } else {
                    return { value: undefined, done: true };  // 迭代完成
                }
            }
        };
    }
}

const numRange = new MyRange(1, 5);

console.log("遍历 MyRange:");
for (const n of numRange) {
    console.log(n);
}
// 输出:
// 1
// 2
// 3
// 4
// 5

// 也可以用于展开运算符
const rangeArray = [...numRange];
console.log("展开运算符:", rangeArray); // 输出: [1, 2, 3, 4, 5]

// 也可以用于 Array.from()
const arrayFromRange = Array.from(numRange);
console.log("Array.from():", arrayFromRange); // 输出: [1, 2, 3, 4, 5]

在这个例子中,MyRange 类实现了 [Symbol.iterator]() 方法,该方法返回一个具有 next() 方法的对象。每次调用 next(),它都会生成范围内的下一个数字,直到达到 end 值。

动手实践: 尝试修改 MyRange 的构造函数,使其可以接受一个步长(step)参数,例如 new MyRange(0, 10, 2),然后让它每次递增 step 值。

4. 生成器 (Generators):简化迭代器创建

手动创建迭代器(如上述 MyRange 示例)有时会比较繁琐,需要手动管理 current 值和 done 状态。ES6 引入的生成器函数 (Generator Functions) 提供了一种更简洁、更强大的方式来创建迭代器。

生成器函数使用特殊的 function* 语法定义,并在其内部使用 yield 关键字来暂停执行并返回一个值。每次调用生成器的 next() 方法,它会从上次 yield 的地方继续执行,直到遇到下一个 yield 或函数结束。

4.1 生成器函数基础

function* countUpTo(limit) {
    let i = 1;
    while (i <= limit) {
        yield i; // 暂停函数执行,并返回 i 的值
        i++;
    }
}

const myGenerator = countUpTo(3); // 调用生成器函数返回一个生成器对象 (也是一个迭代器)

console.log(myGenerator.next()); // { value: 1, done: false }
console.log(myGenerator.next()); // { value: 2, done: false }
console.log(myGenerator.next()); // { value: 3, done: false }
console.log(myGenerator.next()); // { value: undefined, done: true }

生成器对象本身就是可迭代的,所以可以直接用 for...of 循环。

console.log("遍历生成器:");
for (const n of countUpTo(5)) {
    console.log(n);
}
// 输出: 1, 2, 3, 4, 5

4.2 使用生成器实现 MyRange

使用生成器可以大大简化 MyRange 类的 [Symbol.iterator] 实现:

class MyGeneratorRange {
    constructor(start, end) {
        this.start = start;
        this.end = end;
    }

    // 实现可迭代协议,使用生成器函数
    *[Symbol.iterator]() { // 注意这里的 *
        for (let i = this.start; i <= this.end; i++) {
            yield i; // 每次 yield 都会暂停并返回一个值
        }
    }
}

const genRange = new MyGeneratorRange(1, 5);
console.log("\n遍历 MyGeneratorRange:");
for (const n of genRange) {
    console.log(n);
}
// 输出: 1, 2, 3, 4, 5

使用 function* [Symbol.iterator]() 这种语法糖,使得 yield 能够直接控制 for...of 循环的遍历行为,而无需手动编写 next() 方法。

4.3 yield* 委托 (Yield Delegation)

yield* 表达式用于将生成器委托给另一个可迭代对象(通常是另一个生成器)。它会遍历被委托的可迭代对象,并 yield 出其所有值。

function* generatorA() {
    yield 1;
    yield 2;
}

function* generatorB() {
    yield 3;
    yield* generatorA(); // 委托给 generatorA
    yield 4;
}

for (const val of generatorB()) {
    console.log(val);
}
// 输出: 3, 1, 2, 4

5. 迭代器的高级应用与优势

5.1 惰性求值 (Lazy Evaluation)

迭代器的一个关键优势是它支持惰性求值。值只有在需要时才生成和计算,而不是预先生成所有值。这对于处理大型数据集、无限序列或节省资源至关重要。

function* infiniteNumbers() {
    let i = 0;
    while (true) { // 一个无限序列生成器
        yield i++;
    }
}

const gen = infiniteNumbers();
console.log(gen.next().value); // 0
console.log(gen.next().value); // 1
// 我们可以按需获取值,而不需要一次性计算所有值
// 如果我们尝试用 for...of 遍历这个无限生成器,将会陷入死循环!
// for (const num of infiniteNumbers()) { /* 会无限循环 */ }
5.2 展开运算符 (...) 和解构赋值

可迭代对象可以与展开运算符和解构赋值结合使用,提供极大的便利。

const myArray = [1, 2, 3];
const newArray = [...myArray, 4, 5]; // 展开运算符
console.log(newArray); // 输出: [1, 2, 3, 4, 5]

const [first, second, ...rest] = myArray; // 解构赋值
console.log(first, second, rest); // 输出: 1 2 [3]

const myRange = new MyRange(1, 3);
const rangedArray = [...myRange]; // 自定义可迭代对象也能用展开运算符
console.log(rangedArray); // 输出: [1, 2, 3]
5.3 Array.from()Map / Set 构造函数

Array.from() 可以将任何可迭代对象或类数组对象转换为真正的数组。MapSet 的构造函数也接受可迭代对象作为参数。

const myMap = new Map([['a', 1], ['b', 2]]);
const mapKeys = Array.from(myMap.keys()); // myMap.keys() 返回一个迭代器
console.log(mapKeys); // 输出: ['a', 'b']

const mySet = new Set(['x', 'y', 'z']);
const setValues = Array.from(mySet);
console.log(setValues); // 输出: ['x', 'y', 'z']

const stringToArray = Array.from("Hello");
console.log(stringToArray); // 输出: ['H', 'e', 'l', 'l', 'o']

6. 迭代器与生成器的优点

  • 统一接口: 提供了一种通用的方式来遍历各种数据结构,无论其内部实现如何。
  • 惰性求值: 仅在需要时才生成值,节省内存和计算资源,尤其适用于大型或无限序列。
  • 代码简洁性: for...of 循环和生成器函数的 yield 关键字使得遍历逻辑更易读、更简洁。
  • 可组合性: 迭代器和生成器可以很容易地进行组合和管道化,实现复杂的序列处理。
  • 分离关注点: 迭代器将数据存储和遍历逻辑分离,提高了模块化。

7. 迭代器与生成器的注意事项

  • 一次性消费: 大多数迭代器是有状态的,并且是一次性消费的。一旦迭代完成(done: true),你不能再从头开始遍历它,需要重新获取一个新的迭代器实例。生成器函数每次调用都会返回一个新的生成器对象,因此可以重复调用生成器函数来获得新的迭代。
    const numbersIterator = [1, 2][Symbol.iterator]();
    numbersIterator.next(); // { value: 1, done: false }
    numbersIterator.next(); // { value: 2, done: false }
    numbersIterator.next(); // { value: undefined, done: true }
    numbersIterator.next(); // 再次调用,仍然是 { value: undefined, done: true }
    
    // 如果想重新遍历,需要再次获取迭代器
    const anotherNumbersIterator = [1, 2][Symbol.iterator]();
    
  • 性能: 对于简单的数组遍历,传统的 for 循环在某些微观基准测试中可能会略快。但在大多数实际应用中,迭代器带来的代码可读性和维护性收益远超这点微小的性能差异。
  • 错误处理: 在生成器中,未捕获的错误会终止生成器。

8. 最佳实践

  1. 优先使用 for...of 当你需要遍历任何可迭代对象的值时,for...of 是最简洁、最现代的选择。
  2. 使用 letconst 声明 for...of 循环变量: 避免 var 带来的作用域问题。
  3. 为自定义可迭代对象使用生成器: 如果你的自定义数据结构需要遍历功能,定义一个 function* [Symbol.iterator]() 是实现可迭代协议最推荐和最优雅的方式。
  4. 理解惰性求值: 善用生成器来处理大型或无限数据集,避免不必要的内存消耗。
  5. 注意迭代器的“一次性消费”特性: 如果需要多次遍历,请确保每次都获取一个新的迭代器实例。
  6. 善用其他可迭代特性: 结合展开运算符、解构赋值和 Array.from() 来充分发挥迭代器的威力。

9. 总结与展望

迭代器和可迭代协议是 JavaScript ES6 引入的强大机制,它们提供了一种统一、标准化的方式来遍历各种数据结构。

  • 可迭代对象 (Iterable): 拥有 [Symbol.iterator]() 方法,该方法返回一个迭代器。
  • 迭代器 (Iterator): 拥有 next() 方法,该方法返回 { value: ..., done: ... } 对象。
  • for...of 循环: 遍历可迭代对象的首选方式。
  • 生成器函数 (Generator Functions): 使用 function*yield 关键字,极大简化了迭代器的创建过程。

掌握迭代器不仅能让您高效地处理现有集合,还能让您为自己的数据结构赋予强大的遍历能力。通过理解惰性求值和生成器的力量,您将能够编写出更灵活、更节省资源、更具表达力的 JavaScript 代码,从而在现代 JavaScript 开发中脱颖而出。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐