生成器与迭代器

能暂停、能恢复的函数,数据流的控制艺术

🎯 学习目标

学完本章,你将能够:

  • 能够解释迭代协议(Iterable 和 Iterator),并创建自定义可迭代对象
  • 能够使用 function* 和 yield 定义生成器函数,实现惰性求值
  • 能够使用 yield* 委托调用其他生成器,组合多个数据流
  • 能够使用生成器实现无限序列和树形结构遍历
  • 能够使用异步生成器(async function*)处理流式数据

一个普通函数做不到的事

怎么让函数"说到一半停下来",稍后再继续?

// 普通函数:一跑到底,中途停不下来
function normalLoop() {
    let result = [];
    for (let i = 0; i < 1000000; i++) {
        result.push(i * 2);
    }
    return result; // 一次性返回 100 万个元素
}

// 生成器:按需产出,要一个给一个
function* lazyLoop() {
    let i = 0;
    while (true) {
        yield i++ * 2; // 每次只产出一个
    }
}
let gen = lazyLoop();
console.log(gen.next().value); // 0
console.log(gen.next().value); // 2
console.log(gen.next().value); // 4

普通函数要么一口气跑完,要么不跑。但有时候你需要的是"按需产出"——用到多少算多少,不浪费内存。

这就是生成器(Generator)的能力:能暂停、能恢复、能按需产出数据。

Part 1: 迭代器

生成器是迭代器的一种实现,先理解迭代器协议,后面就水到渠成

刚才说生成器能"按需产出",但这套机制底层靠的是什么?答案是一个叫迭代器协议的东西。只要一个对象有 next() 方法、返回 { value, done },它就是迭代器——不管是数组、字符串,还是你自己手写的对象。搞懂这套协议,后面学生成器就像拆礼物一样轻松。

迭代协议

只要实现 next(),就是迭代器

可迭代协议:一个对象如果实现了 [Symbol.iterator]() 方法,它就是"可迭代的"。数组、字符串、Map、Set 都内置了这个方法。

迭代器协议:一个对象如果有 next() 方法,且返回 { value, done } 格式,它就是迭代器。

// 手动实现一个可迭代对象
let range = {
    from: 1,
    to: 5,
    [Symbol.iterator]() {
        let current = this.from;
        let last = this.to;
        return {
            next() {
                return current <= last
                    ? { value: current++, done: false }
                    : { done: true };
            }
        };
    }
};
for (let num of range) console.log(num); // 1, 2, 3, 4, 5
console.log([...range]); // [1, 2, 3, 4, 5]

Part 2: 生成器

手写迭代器太麻烦,生成器是 JS 提供的语法糖

上一节手写的那个 range 迭代器,光是 [Symbol.iterator] 和 next() 就写了一堆样板代码。有没有更简单的方式?有,就是 function* 生成器。只需在函数里写 yield,JS 引擎自动帮你实现迭代器协议。接下来看看生成器怎么声明、怎么暂停恢复、怎么产出无限序列。

function* 与 yield

星号声明,yield 交出值

生成器函数用 function* 声明。函数体内用 yield 暂停执行并产出一个值。每次调用 .next() 恢复执行到下一个 yield。

function* greet() {
    yield "Hello";
    yield "World";
    return "Done";
}

let g = greet();
console.log(g.next()); // { value: "Hello", done: false }
console.log(g.next()); // { value: "World", done: false }
console.log(g.next()); // { value: "Done", done: true }

生成器天然是可迭代的,可以直接用于 for...of 和展开运算符:

function* colors() {
    yield "red"; yield "green"; yield "blue";
}
for (let c of colors()) console.log(c); // red, green, blue

暂停与恢复

yield 是一个"断点"

图:生成器 yield 暂停与恢复执行流程

每次 yield 就像按了暂停键。.next() 就是按播放键。函数内部的状态(变量值、执行位置)在暂停期间被完整保留。

无限序列

while(true) 不再是死循环

function* idGenerator() {
    let id = 1;
    while (true) {
        yield id++;
    }
}

let gen = idGenerator();
console.log(gen.next().value); // 1
console.log(gen.next().value); // 2
console.log(gen.next().value); // 3
// 什么时候要,什么时候生成——永远不会溢出

用生活场景说:普通函数就像一口大锅,一次把饭全煮好。生成器就像自助餐,你点一道菜,厨师做一道,用多少做多少。

这个比喻只是为了帮助理解。生成器实际上是通过协程机制实现的——函数执行上下文在 yield 时被挂起(suspended),不是真的有厨师在等你点菜。

yield* 委托

把产出工作交给另一个生成器

function* inner() {
    yield "a";
    yield "b";
}

function* outer() {
    yield 1;
    yield* inner(); // 委托给 inner
    yield 3;
}

console.log([...outer()]); // [1, "a", "b", 3]

yield* 的含义是:这里所有值由另一个生成器产出,我等它完成后再继续。

Part 3: 控制生成器

生成器不只是被动产出值,还能从外部接收值、被提前终止

前面的生成器都是"我给什么你拿什么"的单向输出。但如果我想往生成器里塞数据呢?或者中途叫它别跑了?甚至给它丢个错误进去?.next(value)、.return()、.throw() 就是这三把钥匙。掌握了它们,你就能从外部完全控制生成器的执行流程。

next 传值

从外部往生成器里"喂"数据

function* accumulate() {
    let sum = 0;
    while (true) {
        let input = yield sum; // yield 暂停并产出 sum
        // next(value) 传入的值赋给 input
        if (input === null) break;
        sum += input;
    }
    return sum;
}

let acc = accumulate();
acc.next();       // 启动,返回 { value: 0, done: false }
acc.next(10);     // input=10, sum=10, 返回 { value: 10, done: false }
acc.next(20);     // input=20, sum=30, 返回 { value: 30, done: false }
acc.next(null);   // input=null, break, 返回 { value: 30, done: true }
易错点:第一次 .next() 只是启动生成器,传入的值会被忽略。从第二次 .next(value) 开始,值才会赋给 yield 左边的变量。

return 与 throw

提前结束和注入错误

// return(value) —— 提前终止生成器
function* gen() {
    yield 1; yield 2; yield 3;
}
let g = gen();
g.next();          // { value: 1, done: false }
g.return("end");   // { value: "end", done: true }
g.next();          // { value: undefined, done: true }

// throw(error) —— 在 yield 处抛出错误
function* errorGen() {
    try {
        yield 1;
        yield 2;
    } catch (e) {
        console.log("捕获:", e.message);
    }
    yield 3;
}
let eg = errorGen();
eg.next();  // { value: 1 }
eg.throw(new Error("oops")); // "捕获: oops"
// 返回 { value: 3, done: false }

Part 4: 实战应用

生成器在实际开发中有哪些用武之地?

理论学了一堆,该看看生成器能解决什么实际问题了。斐波那契数列不用预分配数组、树形结构遍历不用手动维护栈、大数据分块处理不会撑爆内存——这些场景用生成器写起来既简洁又高效。接下来逐个击破。

斐波那契

惰性计算无限数列

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

let fib = fibonacci();
for (let i = 0; i < 10; i++) {
    console.log(fib.next().value);
}
// 0, 1, 1, 2, 3, 5, 8, 13, 21, 34

如果用普通函数,你得先算出所有值存到数组里。生成器只算你需要的,内存占用恒定。

树遍历与分块

递归结构和大数据的利器

// 树遍历(前序)
function* traverseTree(node) {
    yield node.value;
    if (node.left) yield* traverseTree(node.left);
    if (node.right) yield* traverseTree(node.right);
}

// 分块处理大数据
function* chunk(array, size) {
    for (let i = 0; i < array.length; i += size) {
        yield array.slice(i, i + size);
    }
}
let data = Array.from({length: 100}, (_, i) => i);
for (let batch of chunk(data, 10)) {
    console.log(batch); // 每批 10 个
}

Part 5: 异步生成器

异步 + 生成器 = 流式处理数据的终极武器

前面处理的都是同步数据。可真实项目里,数据往往来自网络请求、文件读取这些异步操作。能不能让生成器一边等数据、一边产出结果?async function* 就是为此而生——内部用 await 等数据,外部用 for await...of 消费。再加上管道式组合,流式数据处理变得异常优雅。

异步生成器

yield + await,双剑合璧

async function* fetchPages(url) {
    let page = 1;
    while (true) {
        let res = await fetch(`${url}?page=${page}`);
        let data = await res.json();
        if (data.items.length === 0) return;
        yield data.items;
        page++;
    }
}

async function getAllItems() {
    let all = [];
    for await (let items of fetchPages("/api/items")) {
        all.push(...items);
    }
    return all;
}

异步生成器用 async function* 声明。内部可以用 await,外部用 for await...of 消费。

流式管道

组合异步生成器,像管道一样处理数据

// 过滤
async function* filterStream(source, predicate) {
    for await (let item of source) {
        if (predicate(item)) yield item;
    }
}

// 映射
async function* mapStream(source, mapper) {
    for await (let item of source) {
        yield mapper(item);
    }
}

// 使用——像 Linux 管道一样
async function* numbers() {
    for (let i = 1; i <= 100; i++) {
        yield i;
    }
}

let pipeline = filterStream(
    mapStream(numbers(), n => n * 2),
    n => n > 50
);

for await (let val of pipeline) {
    console.log(val); // 52, 54, 56, ..., 200
}

常见生成器错误

生成器只会执行到第一个 yield,不会自动跑完。 必须反复调用 .next() 或用 for...of 来驱动。

问题一:忘记驱动生成器

// 错误:创建了生成器但没有消费
function* gen() { yield 1; yield 2; }
gen(); // 什么都不会发生!

// 正确:用 for...of 或手动 next()
for (let v of gen()) console.log(v);

问题二:混淆 return 和 yield 的值

function* gen() {
    yield 1;
    return 2; // return 的值 done=true
}
let g = gen();
g.next(); // { value: 1, done: false }
g.next(); // { value: 2, done: true }
// for...of 不会迭代 return 的值!
for (let v of gen()) console.log(v); // 只输出 1

惰性管道

用生成器组合数据处理管线

// 惰性 map
function* lazyMap(iterable, mapper) {
    for (let item of iterable) {
        yield mapper(item);
    }
}

// 惰性 filter
function* lazyFilter(iterable, predicate) {
    for (let item of iterable) {
        if (predicate(item)) yield item;
    }
}

// 惰性 take
function* lazyTake(iterable, n) {
    let count = 0;
    for (let item of iterable) {
        if (count++ >= n) break;
        yield item;
    }
}

把多个生成器串起来,形成惰性管道——数据像水流一样逐个通过每个环节:

// 组合管道:取 1~100 中偶数,平方,取前 5 个
const source = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
                11, 12, 13, 14, 15, 16, 17, 18, 19, 20];

const pipeline = lazyTake(
    lazyMap(
        lazyFilter(source, x => x % 2 === 0),
        x => x * x
    ),
    5
);

console.log([...pipeline]); // [4, 16, 36, 64, 100]
对比数组方法:[...source].filter(...).map(...).slice(0,5) 会立即处理全部 20 个元素。生成器管道只处理到第 10 个元素就满足了条件(找到 5 个偶数并平方),后续元素根本不会触及。数据量越大,惰性优势越明显。

自定义可迭代

让任意类支持 for...of 遍历

class LinkedList {
    constructor() {
        this.head = null;
        this.tail = null;
    }

    append(value) {
        let node = { value, next: null };
        if (!this.head) {
            this.head = node;
        } else {
            this.tail.next = node;
        }
        this.tail = node;
        return this;
    }

    // 实现 Symbol.iterator,让链表可迭代
    [Symbol.iterator]() {
        let current = this.head;
        return {
            next() {
                if (current) {
                    let value = current.value;
                    current = current.next;
                    return { value, done: false };
                }
                return { done: true };
            }
        };
    }
}

let list = new LinkedList();
list.append("Node.js").append("Python").append("Go");

// 可用于 for...of
for (let lang of list) {
    console.log(lang); // Node.js, Python, Go
}

// 可用于展开运算符和解构赋值
console.log([...list]);       // ["Node.js", "Python", "Go"]
let [first, ...rest] = list;
console.log(first);           // "Node.js"
console.log(rest);            // ["Python", "Go"]
易错点:迭代器的 [Symbol.iterator] 必须返回一个带有 next() 方法的对象。如果忘记在 next() 中返回 { done: true },for...of 会变成无限循环。

试一试

练习一

任务:写一个生成器函数 range(start, end),依次产出从 start 到 end 的整数。

// for (let n of range(3, 7)) console.log(n);
// 输出: 3, 4, 5, 6, 7
// 你的代码:

参考答案

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

练习二

任务:写一个生成器 take(gen, n),从另一个生成器中只取前 n 个值。

// let nums = take(fibonacci(), 5);
// console.log([...nums]); // [0, 1, 1, 2, 3]
// 你的代码:

参考答案

function* take(gen, n) {
    for (let i = 0; i < n; i++) {
        let result = gen.next();
        if (result.done) break;
        yield result.value;
    }
}

练习三

任务:实现生成器函数 zip(...iterables),将多个可迭代对象"拉链式"合并。以最短的可迭代对象为准停止产出。

// let zipped = zip([1, 2, 3], ['a', 'b', 'c']);
// console.log([...zipped]);
// [[1, 'a'], [2, 'b'], [3, 'c']]
// 你的代码:

参考答案

function* zip(...iterables) {
    let iterators = iterables.map(it => it[Symbol.iterator]());
    while (true) {
        let results = iterators.map(it => it.next());
        if (results.some(r => r.done)) break;
        yield results.map(r => r.value);
    }
}

📝 要点总结

本章核心知识点

迭代协议

Symbol.iterator 定义可迭代对象

生成器函数

function* + yield 实现暂停与恢复

yield* 委托

将执行权交给另一个生成器

无限序列

生成器按需计算,支持无限数据流

异步生成器

async function* 处理异步流数据

下一章

生成器让我们能精确控制数据流。下一章回到面向对象,学习 class 与继承——用更优雅的方式组织代码结构。

1 / X