深入理解 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*) 可以自动帮你生成迭代器。
function*声明一个 生成器函数,调用它不立刻执行函数体,而是返回一个 可迭代对象(即迭代器) 。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...of | for (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]