JavaScript 迭代器

190 阅读4分钟

深入理解 JavaScript 迭代器:从原理到实战

在现代前端开发中,我们经常使用 for...of、展开运算符 ...Array.from() 等语法,但你是否想过:为什么不同的数据结构都能用同一种方式遍历? 这背后的核心机制,就是 迭代器(Iterator)


一、什么是迭代器?为什么需要它?

“在 JavaScript 中,迭代器(Iterator)是一种设计模式,它为各种可迭代对象(如数组、Map、Set、字符串等)提供了一个统一的接口,用于顺序访问其元素。

这个接口的核心是 Symbol.iterator 方法,调用它会返回一个迭代器对象。该对象有一个 next() 方法,每次调用返回 { value, done },表示当前的值和是否遍历完成。

而 生成器函数(function*)是创建迭代器的一种便捷方式。当我们调用 function* 时,函数体不会立即执行,而是返回一个既是迭代器又是可迭代对象的生成器对象。

在生成器函数内部,yield 表达式会暂停函数的执行,并将一个值返回给外部。每次调用 .next(),函数会从上次暂停的位置继续执行,直到遇到下一个 yield 或函数结束。 特别地,yield 不仅可以返回普通值,也可以返回 Promise,这使得生成器可以用于异步流程控制,比如早期的 co 库或 async/await 的实现原理。”

1.1 问题背景

JavaScript 有多种数据结构:

const arr = [1, 2, 3];
const str = "hello";
const set = new Set([1, 2, 3]);
const map = new Map([['a', 1], ['b', 2]]);

它们的内部存储方式完全不同,但我们都希望用统一的方式遍历它们:

for (const item of arr)  // 数组
for (const char of str)  // 字符串
for (const item of set)  // Set
for (const [k, v] of map) // Map

👉 如何实现这种“统一遍历”?答案就是:迭代器协议。


二、迭代器的核心:可迭代协议(Iterable Protocol)

2.1 两个关键概念

概念说明
可迭代对象(Iterable)实现了 [Symbol.iterator]() 方法的对象
迭代器(Iterator).next() 方法的对象,返回 { value, done }

2.2 for...of 的工作原理

当你写:

for (const item of arr) {
  console.log(item);
}

JavaScript 实际执行:

const iterator = arr[Symbol.iterator](); // 获取迭代器
let result = iterator.next();

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

2.3 手写一个迭代器

function makeIterator(array) {
  let nextIndex = 0;
  return {
    next() {
      return nextIndex < array.length
        ? { value: array[nextIndex++], done: false }
        : { done: true };
    }
  };
}

const iter = makeIterator([1, 2, 3]);
console.log(iter.next()); // { value: 1, done: false }
console.log(iter.next()); // { value: 2, done: false }
console.log(iter.next()); // { value: 3, done: false }
console.log(iter.next()); // { done: true }

三、让任意对象支持 for...of

3.1 为什么普通对象不能用 for...of

const obj = { a: 1, b: 2 };
for (const item of obj) {
  // ❌ TypeError: obj is not iterable
}

因为普通对象没有实现 Symbol.iterator 方法。

3.2 让对象“可迭代”

const obj = {
  a: 1,
  b: 2,
  [Symbol.iterator]() {
    const values = Object.values(this);
    let index = 0;
    return {
      next() {
        if (index < values.length) {
          return { value: values[index++], done: false };
        } else {
          return { done: true };
        }
      }
    };
  }
};

for (const val of obj) {
  console.log(val); // 1, 2
}

四、生成器:迭代器的语法糖

手动写迭代器太繁琐?生成器函数(function* 可以自动帮你生成迭代器。

  1. function* 声明一个 生成器函数,调用它不立刻执行函数体,而是返回一个 可迭代对象(即迭代器) 。
  2. yield 是函数体内的 “暂停/产出”运算符,每执行到 yield 值 就把值交出去,并冻结当前状态,等外界再次调用 .next() 才从暂停处继续往下跑。yield* 的作用是:把一个“可迭代对象(iterable)”中的每一个值,逐个 yield 出来,而不是整体作为一个值返回。

4.1 基本用法

function* gen() {
  yield 1;
  yield 2;
  yield 3;
}

const iter = gen();
iter.next(); // { value: 1, done: false }
iter.next(); // { value: 2, done: false }

4.2 实现 range(1, 5)

function* range(from, to) {
  for (let i = from; i <= to; i++) {
    yield i;
  }
}

for (const n of range(1, 3)) {
  console.log(n); // 1, 2, 3
}

五、yield*:委托给另一个可迭代对象

yield* 可以将遍历任务“委托”给另一个可迭代对象。

function* gen() {
  yield* [1, 2];
  yield* 'ab';
  yield* new Set([3, 4]);
}

for (const x of gen()) {
  console.log(x); // 1, 2, 'a', 'b', 3, 4
}

六、哪些语法依赖迭代器?

任何接受“可迭代对象”的语法,都会调用其 Symbol.iterator 方法:

语法示例
for...offor (const x of arr)
展开运算符 ...const newArr = [...arr]
数组解构const [a, b] = arr
Array.from()Array.from(iterable)
new Map() / new Set()new Set([1,2,3])
Promise.all()Promise.all([p1, p2])

七、经典面试题:让对象支持数组解构

题目

const [a, b] = { c: 1, d: 2 };
console.log(a, b); // 如何输出 1, 2?

解法

Object.prototype[Symbol.iterator] = function* () {
  for (const key of Object.keys(this)) {
    yield this[key];
  }
};

const [a, b] = { c: 1, d: 2 };
console.log(a, b); // 1, 2 ✅

八、实际应用场景

8.1 惰性求值(Lazy Evaluation)

function* fibonacci() {
  let [a, b] = [0, 1];
  while (true) {
    yield a;
    [a, b] = [b, a + b];
  }
}

const fib = fibonacci();
console.log(fib.next().value); // 0
console.log(fib.next().value); // 1
// 可以无限生成,但只在需要时计算

8.2 合并有序数组(归并排序思想)

function* mergeSorted(arr1, arr2) {
  let i = 0, j = 0;
  while (i < arr1.length && j < arr2.length) {
    yield arr1[i] < arr2[j] ? arr1[i++] : arr2[j++];
  }
  while (i < arr1.length) yield arr1[i++];
  while (j < arr2.length) yield arr2[j++];
}

console.log([...mergeSorted([1,3,5], [2,4,6])]); // [1,2,3,4,5,6]