Iterator 和 Generator
2025-10-29 12:58
“Generator,让函数一次只专注于当前的值,就像迭代器按步取物,也像我们整理思绪时,一次只理清一条线索。”
在学习 async/await 时有提到过 async/await函数是通过类似 Generator 函数实现的。所以这张我们来学习一下 Generator 函数。
生成器(Generator)是 ES6 中引入的语言特性,其本质是一个可以暂停和恢复执行的函数。生成器是一类特殊的迭代器(Iterator)。所以要了解生成器,首先我们要学习迭代器的概念。
什么是迭代器 (Iterator)
在 JavaScript 中,迭代器(Iterator) 是一种用于按顺序访问集合元素的机制。 它提供了一个统一的接口,让不同的数据结构(数组、字符串、Set、Map 等)都可以被“逐项读取”。
一个对象只要实现了 next() 方法,就可以称为迭代器:
JSconst iterator = { next() { return { value: "A", done: false }; }, }; function createIterator(arr) { let index = 0; return { next() { if (index < arr.length) { return { value: arr[index++], done: false }; } else { return { value: undefined, done: true }; } }, }; } const it = createIterator(["a", "b", "c"]); console.log(it.next()); // { value: 'a', done: false } console.log(it.next()); // { value: 'b', done: false } console.log(it.next()); // { value: 'c', done: false } console.log(it.next()); // { value: undefined, done: true }
这就是迭代器的最小实现。
每次 next() 都返回当前值,并记录内部位置。
可迭代对象
如果一个对象拥有 [Symbol.iterator] 方法,并且这个方法返回一个迭代器对象,那么它就是一个 可迭代对象(Iterable)。
JSconst iterable = { data: ["x", "y", "z"], [Symbol.iterator]() { let index = 0; return { next: () => { if (index < this.data.length) { return { value: this.data[index++], done: false }; } else { return { done: true }; } }, }; }, }; for (const item of iterable) { console.log(item); // x y z }
for...of、spread (...)、Array.from()等语法都依赖这个接口。
迭代器模式
迭代器模式:一种设计模式,用于统一迭代过程。把拥有 Iterable 接口的数据结构称为可迭代对象(iterable),而且可以通过迭代器 Iterator 消费。
- 任何实现 Iterable 接口的数据结构 都可以 被实现 Iterator 接口的结构 消费(consume)。
- 迭代器(iterator)是按需创建的一次性对象。每个迭代器都会关联一个可迭代对象,而迭代器 会暴露 其关联的可迭代对象 中与迭代相关的 API。
- 迭代器无须了解与其关联的可迭代对象的结构,只需要知道如何取得连续的值。 这种概念上的分离正是 Iterable 和 Iterator 的强大之处。
可迭代协议
可迭代协议要求实现 Iterable 接口需要同时具备两种能力:
- 支持迭代的自我识别能力
- 创建实现 Iterator 接口的对象的能力
因此在 JS 中为满足 Iterable 接口的要求,使用特殊的 Symbol.iterator 作为属性名,属性值是默认迭代器,其引用一个迭代器工厂函数(iterator creator),调用这个工厂函数会返回一个新迭代器。
迭代器协议
迭代器是一种一次性使用的对象,用于迭代与其关联的可迭代对象。迭代器上拥有 next()方法,该方法用于得到下一个数据,调用该方法可在其关联的可迭代对象中实现遍历数据操作。
每次成功调用 next(),都会返回一个 IteratorResult 对象,该对象包含了 done 和 value 两个属性。
- done 是一个布尔值,表示是否还可以再次调用 next()取得下一个值;done: true 状态称为“耗尽”。
- value 包含可迭代对象的下一个值(done 为 false),当 done 为 true value 肯定为 undefined
JS// IteratorResult 对象格式 { value: '',//可迭代对象的数据 done: Boolean,//表示是否迭代完成 }
使用迭代器遍历其关联的可迭代对象时的注意点:
- 调用迭代器 next()方法按顺序迭代了数组,直至不再产生新值。这个过程中迭代器并不知道怎么从可迭代对象中取得下一个值,也不知道可迭代对象有多大。只要迭代器到达 done: true 状态,后续再调用 next()就一直返回 { value: undefined, done: true }。
- 每个迭代器都表示对可迭代对象的一次性有序遍历。不同迭代器的实例相互之间没有联系,只会独立地遍历可迭代对象。
- 如果可迭代对象在迭代期间被修改了,那么迭代器也会做出相应的变化。
JS// 可迭代对象 用一个简单的数组来表示 let arr = ["foo", "bar"]; // 迭代器工厂函数 console.log(arr[Symbol.iterator]); // f values() { [native code] } // 迭代器 let iter = arr[Symbol.iterator](); console.log(iter); // ArrayIterator {} // 执行迭代 console.log(iter.next()); // {value: 'foo', done: false} arr.splice(1, 0, "baz"); // 在数组中间插入值 console.log(iter.next()); // { value: 'baz', done: false } console.log(iter.next()); // { value: 'bar', done: false } console.log(iter.next()); // { value: undefined, done: true } console.log(iter.next()); // { value: undefined, done: true }
迭代器 产生的原因
ES5 之前 循环遍历方法
在 ES5 之前,我们只能通过循环来遍历一个数组:
JSlet arr = [1, 2, 3, 4, 5]; for (let i = 0; i < arr.length; i++) { console.log(arr[i]); // 1 2 3 4 5 }
此时的严重不足是:
- 迭代之前需要事先知道如何使用数据结构
- 遍历顺序并不是数据结构固有的 此方法只适用于带有索引的数据类型,并且还得通过数组对象来获取值,该方法得通用性不强,无法适用于 ES6 新增的例如 Set 类型。
ES5 forEach 遍历方法
ES5 新增了 Array.prototype.forEach 方法,使用方法如下:
JSlet arr = [1, 2, 3, 4, 5]; arr.forEach((item) => console.log(item)); //1 2 3 4 5
此时的严重不足是:
- 虽然这个方法向通用迭代需求迈进了一步,解决了单独记录索引和通过数组对象取得值的问题,但没有办法标识迭代何时终止。
- 并且这个方法也只适用于数组,而且回调结构也比较笨拙,会消耗一定的性能。
因此为了解决上述问题,ES6 引入了 迭代器模式。
什么是生成器 (Generator)
Generator 是一种可以随时暂停和恢复执行的函数。由 function* 定义,通过 yield 语句实现“暂停点”。它不仅生成值,还本质上是一个 迭代器(Iterator),让函数像迭代器一样按步执行,并且可以和外部交互。
JSfunction* hello() { console.log("Hi"); yield "Step 1"; yield "Step 2"; console.log("Done"); } const it = hello(); console.log(it.next()); // "Hi" -> { value: 'Step 1', done: false } console.log(it.next()); // { value: 'Step 2', done: false } console.log(it.next()); // "Done" -> { value: undefined, done: true }
Generator 每次调用 next(),都会从上次 yield 停下的地方继续执行。
Generator 是创建迭代器的工厂
JSfunction* gen() { yield 1; yield 2; } const it = gen(); console.log(typeof it.next); // function -> 它就是迭代器 // 每个 Generator 调用返回的对象本身就是一个迭代器,它遵循 迭代器协议,可以被 for...of 遍历: for (const val of gen()) { console.log(val); // 1 2 }
无限序列生成
Generator 可以轻松创建无限序列,这是普通迭代器难以做到的:
JSfunction* idMaker() { let id = 1; while (true) { yield id++; } } const ids = idMaker(); console.log(ids.next().value); // 1 console.log(ids.next().value); // 2 console.log(ids.next().value); // 3
Generator 与外部交互
Generator 可以通过 next(value) 接收外部传入的值,实现函数与外界的数据交互:
JSfunction* greeting() { const name = yield "What's your name?"; yield `Hello, ${name}!`; } const it2 = greeting(); console.log(it2.next().value); // "What's your name?" console.log(it2.next("Alice").value); // "Hello, Alice!"
这体现了 Generator 的暂停与恢复能力:暂停执行的同时可以接收外部信息。
但是有个问题是在 Generator 中,第一次调用 next() 时传入的参数会被忽略,因为第一次 next() 是启动 Generator 并执行到第一个 yield 停下的,Generator 内部还没有暂停,所以无法接收值。
JSfunction* listener() { console.log("你说,我在听..."); while (true) { let msg = yield; console.log("我听到你说:", msg); } } let l = listener(); l.next("在吗?"); // 你说,我在听... l.next("你在吗?"); // 我听到你说: 你在吗? l.next("芜湖!"); // 我听到你说: 芜湖!
这是因为 第一次的 next() 调用被忽略,因为 Generator 函数内部还没有开始执行。从第二次开始 next() 才被执行,此时传入的参数会被接收。
第一次的
next()可以被理解为开始调用 Generator。
Generator 异常处理
Generator 可以在内部捕获异常,或者通过迭代器对象抛入异常:
JSfunction* numbers() { try { yield 1; yield 2; } catch (e) { console.log("Caught:", e.message); } } const it3 = numbers(); console.log(it3.next().value); // 1 it3.throw(new Error("Oops")); // Caught: Oops
最后
在 JavaScript 的设计体系里:
- 迭代器(Iterator) 是一种“协议”,规定了“一个对象如何被顺序访问”。
- 生成器(Generator) 是一种“语法糖”,帮助你轻松创建迭代器,并增加暂停、恢复、外部交互能力。
- 迭代器是规范,生成器是实现;迭代器只关注顺序访问,生成器让函数可以按步执行、接收外部输入、甚至处理异常。
也就是说:
迭代器是规范,生成器是实现。
当函数学会暂停,它就能更专注于当前的值,也更善于与外界互动,就像我们整理思绪时,一次只理清一条线索。