【重走JavaScript之高级程序设计】迭代器与生成器
1 迭代器
-
什么是迭代?
答:按照顺序反复多次执行一段程序,通常有明确的终止条件
-
JavaScript为什么需要像Python、Java、C++新增了迭代器模式
答:需要事先知道如何使用数据结构,遍历顺序并不是数据结构固有的(比如对象的属性是无序的)
-
什么是迭代器模式?
答:特定结构的可迭代对象,对外暴露了Iterable(可迭代对象)接口,而且可以通过Iterator(迭代器)消费
1.1 可迭代协议
ECMA规定,暴露一个属性作为迭代器,这个属性必须使用特殊的Symbol.iterator作为key。
以下内置类型都实现了Iterable接口,1.字符串、2.数组、3.映射、4.集合、5.arguments对象、6.NodeList等Dom集合类型。
以下代码仅演示和理解迭代器,实际开发不需要这样操作。
let obj = {};
let str = "abc";
let arr = ["a", "b", "c"];
let map = new Map().set("a", 1).set("b", 2);
let set = new Set().add("a").add("b");
let els = document.querySelectorAll("div");
// 未实现Iterable接口
console.log(obj[Symbol.iterator]); // undefiend
// 实现了Iterbale接口,通过了Iterable接口得到了一个函数,这个函数就是迭代器函数
console.log(str[Symbol.iterator]); // ƒ [Symbol.iterator]() { [native code] }
console.log(arr[Symbol.iterator]); // ƒ values() { [native code] }
console.log(map[Symbol.iterator]); // ƒ entries() { [native code] }
console.log(set[Symbol.iterator]); // ƒ values() { [native code] }
console.log(els[Symbol.iterator]); // ƒ values() { [native code] }
// 调用迭代器函数,得到迭代器
console.log(str[Symbol.iterator]); // ƒ [Symbol.iterator]() { [native code] }
console.log(arr[Symbol.iterator]); // ƒ values() { [native code] }
console.log(map[Symbol.iterator]); // ƒ entries() { [native code] }
console.log(set[Symbol.iterator]); // ƒ values() { [native code] }
console.log(els[Symbol.iterator]); // ƒ values() { [native code] }
以下方法可以直接使用上面这些可迭代对象,1.for-of、2.数组解构、3.扩展运算符、4.Array.from()、5.创建集合、6.创建集合和映射、7.Promise.all()和Promise.race()、8.在generator生成器中使用yield*操作符、
let arr = ["a", "b", "c"];
// for-of循环
for (const el of arr) {
console.log(el);
} // a b c
// 数组解构
let [x, y, z] = arr;
console.log(x, y, z); // a b c
// 扩展运算符
let arr2 = [...arr];
console.log(arr2); // ['a', 'b', 'c']
// Array.from()
let arr3 = Array.from(arr);
console.log(arr3); // ['a', 'b', 'c']
// 集合和映射
let set = new Set(arr);
console.log(set); // Set(3) {'a', 'b', 'c'}
let pairs = arr.map((x, i) => [x, i]);
let map = new Map(pairs);
console.log(map); // Map(3) {'a' => 0, 'b' => 1, 'c' => 2}
1.2 迭代器协议
迭代器是一种一次性使用的对象。迭代器API使用next()方法,返回一个IterableResult对象,包含两个属性键名为done和value。
let arr = ["a", "b"]; // 可迭代对象
console.log(arr[Symbol.iterator]); // 迭代器工厂函数,ƒ values() { [native code] }
let iterFirst = arr[Symbol.iterator](); // 调用工厂函数生成的第一个迭代器
let iterSecond = arr[Symbol.iterator](); // 生成的第二个迭代器
console.log(iterFirst.next()); // {value: 'a', done: false}
console.log(iterSecond.next()); // {value: 'a', done: false}
console.log(iterFirst.next()); // {value: 'b', done: false}
console.log(iterSecond.next()); // {value: 'b', done: false}
console.log(iterSecond.next()); // {value: undefined, done: true}
console.log(iterSecond.next()); // {value: undefined, done: true}
console.log(iterSecond.next()); // {value: undefined, done: true}
根据以上代码可知
- 不同迭代器之间不会相互影响,可以独立的迭代对象
- done为一个布尔值只要done为false,则可以继续调用next()方法。value为迭代器返回的当前值。
- 当done为true时,后续调用next()value为undefiend
1.3 自定义迭代器
自定义封装一个迭代器,实现反复计数功能
class Counter {
constructor(limit) {
this.limit = limit;
}
[Symbol.iterator]() {
// 定义[Symbol.iterator]方法,
let count = 1, // 这里通过闭包达到一个效果,即使同时创建多个迭代器时也保证计算器count从1 开始(闭包作用之一,避免全局变量count被污染)
limit = this.limit;
return {
next() {
// 对迭代器使用next()方法
if (count <= limit) {
return { done: false, value: count++ };
} else {
return { done: true, value: undefined };
}
}
};
}
}
let counter = new Counter(5);
for (const el of counter) {
console.log(el);
} // 1 2 3 4 5
1.4 提前终止迭代器
ES5中提出的forEach是没有终止的(除了trycatch),return()方法可以提前终止迭代。
- for-of循环通过break,continue,return或throw提前退出
- 解构操作并未消费所有值
2 生成器
-
什么是生成器?
生成器本质是函数,在函数名称前加一个星号(*)表示为生成器,星号不受两侧空格的影响。调用之后返回一个生成器对象。
-
生成器有什么用?
生成器是ES6新增的结构,拥有在一个函数块内暂停和恢复代码执行的能力。生成器可以自定义迭代器。
2.1 生成器函数的定义
function* generatorFn() {}
let generatorFn = function* () {};
let foo = {
*generatorFn() {}
};
class Foo {
*generatorFn() {}
}
class Bar {
static *generatorFn() {}
}
生成器的调用与执行
function* generatorFn() {
console.log("foo");
return "foo";
}
const result = generatorFn();
console.log(result); // generatorFn {<suspended>},调用生成器函数产生一个生成器对象(暂停执行),直接调用生成器函数并不会执行内部代码
console.log(result.next); // ƒ next() { [native code] },和迭代器一样有next方法
console.log(result.next()); // 控制台打印foo {value: 'foo', done: true},生成器函数只会在初次调用next()方法开始执行,执行内部代码,返回结果类似迭代器
console.log(result.next()); // {value: undefined, done: true},
2.2 yield关键字中断执行
yield关键字可以控制生成器的停止和开始执行。遇到这个关键字,执行停止。停止后只能通过next()方法恢复执行。
function* generatorFn() {
console.log("a");
yield "foo";
console.log("b");
yield "bar";
console.log("c");
return "baz";
console.log("d");
}
const result = generatorFn();
console.log(result.next()); // a {value: 'a', done: false} 遇到yield关键字退出的生成器函数会处在done:false状态。
console.log(result.next()); // b {value: 'b', done: false}
console.log(result.next()); // c {value: 'foo', done: true} 遇到return关键字退出的生成器函数会处在done:true状态。
console.log(result.next()); // {value: undefined, done: true} done的状态已经转变为true,所以字符串d并不会打印
yield关键字使用的注意点
- 和迭代器一样,调用同一个生成器函数生成多个生成器对象,各个对象之间不会影响
- yield关键字只允许直接在生成器函数内部使用,并且不允许嵌套层级。否在会无效
直接显式的调用next()的方法用处不大,如果把生成器对象当成可迭代对象,使用起来会更方便。
function* generatorFn() {
yield 1;
yield 2;
yield 3;
}
for (const iterator of generatorFn()) {
console.log(iterator); // 1 2 3
}
2.3 使用yield实现输入和输出
生成器传递参数
function* generatorFn(init) {
console.log(init);
console.log(yield);
console.log(yield);
}
let result = generatorFn("foo");
result.next("bar"); // foo 第一次调用next传入的值不会被使用,因为这次调用是为了开始执行生成器函数
// result.next() // foo 第一次调用next不传参返回的值和上面代码一样
result.next("baz"); // baz
result.next("qux"); // qux
// 迭代索引
function* nTimes(n) {
for (let i = 0; i < n; i++) {
yield i;
}
}
for (const iterator of nTimes(3)) {
console.log(iterator); // 0 1 2
}
// 实现范围输出
function* range(start, end) {
while (end > start) {
yield start++;
}
}
for (const iterator of range(4, 8)) {
console.log(iterator); // 4 5 6 7
}
// 实现填充数组
function* fill(n) {
while (n--) {
yield 0;
}
}
console.log(Array.from(fill(8))); // [0, 0, 0, 0, 0, 0, 0, 0]
2.4 使用yield*可以一次性迭代可迭代对象
yield* 本质是语法糖,yield*本质上将一个可迭代对象序列化为一连串可以单独产出的值,所以等同于把yield放入循环。
// 这种写法可以直接用 yield* 来代替
// function* generatorFn() {
// for (const iterator of [1, 2, 3]) {
// yield iterator
// }
// }
function* generatorFn() {
yield* [1, 2, 3];
yield* [4, 5];
yield* [6, 7];
}
let result = generatorFn();
for (const iterator of generatorFn()) {
console.log(iterator); // 1,2,3,4,5,6,7
}
yield*关联迭代器返回done:true的value属性,对普通迭代器这个值为undefined。而对于生成器函数来说,这个值就是生成器函数返回的值。
function* generatorFn() {
console.log("iter value", yield* [1, 2, 3]); // yield* 关联普通迭代器
}
for (const iterator of generatorFn()) {
console.log("value", iterator);
}
/*
value 1
value 2
value 3
iter value undefined
*/
function* innerGeneratorFn() {
yield "foo";
return "bar";
}
function* outerGeneratorFn() {
console.log("iter value:", yield* innerGeneratorFn()); // yield* 关联生成器函数
}
for (const iterator of outerGeneratorFn()) {
console.log("value", iterator);
}
/*
value foo
iter value: bar
*/
使用yield*实现递归算法
function* nTimes(n) {
if (n > 0) {
yield* nTimes(n - 1);
yield n - 1;
}
}
for (const iterator of nTimes(3)) {
console.log(iterator); // 0 1 2
}
2.5 生成器作为默认迭代器
for-of调用了默认迭代器(它恰好又是一个生成器函数)并产生了一个生成器对象。这个生成器对象是可迭代的,所以完全可以在迭代中使用。
class Foo {
constructor() {
this.values = [1, 2, 3];
}
*[Symbol.iterator]() {
yield* this.values;
}
}
const f = new Foo();
for (const iterator of f) {
console.log(iterator); // 1 2 3
}
2.6 提前终止生成器
与迭代器类似,生成器也支持可关闭的概念。一个实现Iterator接口的对象一定有next()方法,还有一个可选的return()方法用于提前终止迭代器。除了以上方法还有throw()
2.6.1 return()
return()方法会强制生成器进入关闭状态。return 传入的值就是终止迭代器对象的值
function* generatorFn() {
yield* [1, 2, 3];
}
const result = generatorFn();
console.log(result); // generatorFn {<suspended>}
console.log(result.return(4)); // {value: 4, done: true}
console.log(result); // generatorFn {<closed>}
console.log(result.next()); // {value: undefined, done: true}
console.log(result.next()); // {value: undefined, done: true}
console.log(result.next()); // {value: undefined, done: true}
2.6.2 throw()
throw()方法会在暂停的时候将一个提供的错误注入到生成器对象中。如果错误未被处理,生成器就会关闭。
function* generatorFn() {
yield* [1, 2, 3];
}
const result = generatorFn();
console.log(result); // generatorFn {<suspended>}
try {
result.throw("error");
} catch (error) {
console.log(error); // error
}
console.log(result); // generatorFn {<closed>}
不过,假如生成器函数内部处理了这个错误,那么生成器就不会关闭,而且还可以恢复执行。错误处理会跳过对应的yield。
function* generatorFn() {
for (const iterator of [1, 2, 3]) {
try {
yield iterator;
} catch (error) {}
}
}
const result = generatorFn();
console.log(result.next()); // {value: 1, done: false}
result.throw("foo");
console.log(result.next()); // {value: 3, done: false}
3 实战
3.1 遍历树形结构
// 遍历树形数据结构时,生成器函数能够极大地简化代码。
function* treeRecursion(tree) {
yield tree.value;
if (tree.children) {
for (const child of tree.children) {
yield* treeRecursion(child);
}
}
}
const tree = {
value: "root",
children: [{ value: "children1" }, { value: "children2", children: [{ value: "grandchild" }] }]
};
for (const value of treeRecursion(tree)) {
console.log(value);
}
3.2 控制流
// 生成器可以用于直观地处理复杂的控制流。
function* flowControlController() {
const first = yield "Pls finish First Step";
console.log(first);
const second = yield "Pls finish Second Step";
console.log(second);
return "Done";
}
const controller = flowControlController();
console.log(controller.next()); // { value: 'Pls finish First Step', done: false }
console.log(controller.next("First Step")); // First Step
console.log(controller.next("Second Step")); // Second Step
3.3 暂停和恢复代码
// 在复杂的异步逻辑中,生成器允许我们暂停和恢复代码的执行,特别是在等待异步操作完成时。
function* asyncDataFetcher() {
const data = yield fetchDataFromAPI();
console.log(data);
}
const fetcher = asyncDataFetcher();
fetcher.next().value.then(data => {
fetcher.next(data);
});
3.4 无限生成序列
// 生成器函数可以创建无限的序列,而不需要存储整个序列。
function* infiniteSequence() {
let i = 0;
while (true) {
yield i++;
}
}
const sequence = infiniteSequence();
console.log(sequence.next().value); // 0
console.log(sequence.next().value); // 1
// ... 无限执行下去,每次调用返回一个新值
3.5 搭建通信管道
// 生成器可以实现两个函数间的双向通信。
function* generatorA() {
const valueFromB = yield "Data from A";
console.log("Received data from B:", valueFromB);
}
function* generatorB(genA) {
const valueFromA = genA.next().value;
genA.next("Data from B");
console.log("Received data from A:", valueFromA);
}
const genA = generatorA();
const genB = generatorB(genA);
genB.next();
3.6 批量处理任务
// 对于处理数据的任务可以分批次进行,以节省内存占用。
function* batchProcessor(batchSize) {
let data = null;
while ((data = fetchData(batchSize))) {
yield processBatch(data);
}
}
const processor = batchProcessor(100);
let processedData = processor.next();
while (!processedData.done) {
console.log("Processed batch:", processedData.value);
processedData = processor.next();
}
3.7 生成唯一ID
// 生成器可以用于生成唯一ID序列。
function* uniqueIdGenerator() {
let id = Date.now();
while (true) {
yield id++;
}
}
const ids = uniqueIdGenerator();
console.log(ids.next().value); // 一个基于时间戳的ID
console.log(ids.next().value); // 下一个ID
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列:基于图像分类模型对图像进行分类
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 25岁的心里话
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现