【JS进阶】JS 迭代器和生成器

news/2024/10/17 18:23:45/

JS 迭代器和生成器

1.迭代器

迭代器,Iterator,其代表一种接口,目的是为各种不同的数据结构提供统一的访问机制。

任何数据结构,只要部署了 Iterator 接口,就可以完成遍历操作!(当然要是可遍历的数据结构,像 let、Number 这种不可遍历的自然就不支持迭代器了)

首先来了解三个相关的类型:

  • 可迭代对象 — 类似Array、NodeList等数据结构,当然我们也可以自己定义一个可迭代对象
  • 迭代器对象 — 是指任何具备next()方法的,且方法返回迭代结果对象的对象,其可以用于执行迭代。
  • 迭代器结果对象 — 指由next()方法返回的,具有属性valuedone的对象,用于保存每次迭代的结果。

具体来说,当我们通过使用next()方法实现了一个符合迭代协议的对象时,这个对象就被称为迭代器对象。此时,next()方法会返回一个拥有属性valuedone的对象,即迭代器结果对象。对于迭代器结果对象中的两个属性:value对应的是迭代时的next值;而done则是用于标识是否已经迭代到序列的最后一个值。

1.1 迭代协议

至于迭代协议 Iterator protocol ,官方文档中的大致意思是:

首先,迭代协议并不是新的内置实现或语法,而是协议。这些协议可以被任何遵循某些约定的对象来实现。

迭代协议具体分为两个协议:可迭代协议迭代器协议

可迭代协议允许 JavaScript 对象定义或定制它们的迭代行为。而要成为一个可迭代对象,需要实现**@@iterator** 方法,即一个名为 [Symbol.iterator] 的方法,该方法没有参数,其返回值为一个符合迭代器协议的对象。

当一个对象要被迭代时(例如被放入一个 for of 循环中时),会自动地调用它的**@@iterator** 方法,然后通过该方法返回的迭代器对象获取要迭代的值。

迭代器协议定义了产生一系列值(无论是有限个还是无限个)的标准方式,当值为有限个时,所有的值都被迭代完毕后,则会返回一个默认返回值。简单来说,其定义了一个迭代器对象的实现标准,即:

  1. 该对象必须实现了一个**next()** 方法
  2. 该对象中所有迭代器协议的方法(next()return()throw())都应返回实现 IteratorResult 接口的对象,即拥有属性valuedone的对象

至于更详细的内容,可以参阅官方文档。

1.2 哪些数据结构支持迭代器?

支持 Iterator 接口的数据结构有:

  1. String
  2. Array
  3. Map
  4. Set
  5. TypedArray
  6. 函数中的 arguments 对象
  7. DOM 中的 NodeList 对象

另外,需要注意的是,实际上我们也可以不使用这些数据结构,而是自己定义一个符合上面迭代协议的对象!

1.3 迭代器原理

根据上面的内容,我们可以发现,当我们迭代一个可迭代对象时,实际上是通过该对象身上的**@@iterator** 方法获得一个迭代器对象,然后重复地调用该迭代器对象中的**next()** 方法,而该方法会返回一个迭代器结果对象,其中包含了下一次迭代的值,以及一个标识是否结束的布尔值。重复操作直到该布尔值为 true 时,迭代就停止了。

1.4 迭代器的基本应用

对于可迭代对象,我们实际上并不用显示地调用**@@iterator** 方法,而是可以通过 for of、扩展操作符等直接操作!

// 通过 for of 操作可迭代对象
const Iterable = ['1', '2', '3', '234', '555'];
for (let v of Iterable) {console.log(v)	// '1', '2', '3', '234', '555'
}// 通过扩展操作符操作可迭代对象
data = [1, 2, 3, 4, 5, 6, 7, 8];
console.log(Math.max(...data)); 	// 8

另外,迭代器也可以用于 ES6 中的解构赋值:

let arr = [10 , 18 , 20];
let [a , b , c] = arr;
console.log(c);		// 20
1.5 自定义的可迭代对象

根据前面的内容,要实现一个可迭代对象,首先需要实现**@@iterator** 方法,其需要返回一个内置了**next()** 方法的迭代器对象。next() 方法需要能够实现迭代,并返回一个拥有属性valuedone的迭代器结果对象。

因此,实现如下:

const myIterable = {name: 'my iterable obj',data: ['test1', 'iterable', '222', 'test2', 'ababab'],// 迭代器方法[Symbol.iterator]() {// 索引let index = 0;// 返回一个迭代器对象return {// next()方法next: () => {if (index < this.data.length) {const res = {value: this.data[index],done: false,};index++;return res;} else {return {value: undefined,done: true,};}},};},
};// 通过 for of 遍历该可迭代对象
for (let v of myIterable) {console.log(v)	// 'test1', 'iterable', '222', 'test2', 'ababab'
}

2.生成器

2.1 生成器的基本概念和基本原理

生成器,在官方文档中的描述是这样的:

生成器函数提供了一个强大的选择:它允许你定义一个包含自有迭代算法的函数,同时它可以自动维护自己的状态。

生成器同时也是一种异步编程解决方案。

生成器函数使用**function***语法编写。最初调用时,生成器函数不执行任何代码,而是返回一种称为 Generator 的迭代器。

而通过调用生成器的**next()**方法消耗值时,生成器函数才会执行,并一直运行直到遇到 yield 关键字。即,第一次调用next()方法时,生成器函数会一直执行到其代码中的第一个 yield 关键字处;第二次调用next()方法时,生成器函数会从第一个 yield 关键字处执行到第二个 yield 关键字处。

下面是一个例子:

// 定义一个生成器函数
function* fn() {console.log('生成器函数第一次执行');yield 'result1';console.log('生成器函数第二次执行');yield 'result2';console.log('生成器函数第三次执行');yield 'result3';console.log('生成器函数第四次执行');
}// 调用生成器函数,得到一个生成器对象
let gen = fn()	// 此处生成器函数内部的代码并不会执行!仅仅是得到了一个生成器对象// 调用生成器对象的next()方法
console.log(gen.next())	// 生成器函数第一次执行 {value: 'result1', done: false}
console.log(gen.next())	// 生成器函数第二次执行 {value: 'result2', done: false}
console.log(gen.next())	// 生成器函数第三次执行 {value: 'result3', done: false}
console.log(gen.next())	// 生成器函数第四次执行 {value: undefined, done: true}

可以看到,第一次调用next()方法时,生成器函数内部的代码仅执行到第一个 yield 关键字处,并将 yield 关键字后表达式的值作为next()方法所返回的迭代器结果对象中value的值!

而第二次调用next()方法时,则是从第一个 yield 关键字后开始执行,直到遇到第二个 yield 关键字,并同样地将yield 关键字后表达式的值作为next()方法所返回的迭代器结果对象中value的值返回出去…重复此操作,直到最后一次调用next()方法,此时从最后一个 yield 关键字后开始执行,直到函数结束,且此时返回的值中,value的值为undefined,而done的值为true,说明迭代已经结束!

2.2 生成器函数中的yield

通过上面的例子,我们可以了解到,执行next()方法后,生成器函数会将某一个yield关键字后面的表达式的值作为next()方法的返回值,并将函数暂停在yield关键字处。

而上面的例子中没有提及的是,如果我们在调用next()方法时,传入参数,那么我们传入的参数会变成上一次暂停处的yield表达式的值!

下面通过一个例子来理解:

// 定义一个生成器函数
function* fn(str) {console.log(str); let a = yield 1;  // 第一次调用暂停的位置console.log(a);   let b = yield 2;  // 第二次调用暂停的位置console.log(b);  let c = yield 3;  // 第三次调用暂停的位置console.log(c);return 100
}// 获取生成器对象
let gen = fn('测试传参')// 第一次调用next方法,传入一个参数 'a'
console.log(gen.next('a'))
// 此时是第一次调用,该参数会被丢弃,因为没有上一次暂停处!
// 执行结果为:测试传参 {value: 1, done: false}// 第二次调用,传入一个参数 'b'
console.log(gen.next('b'))
// 此时有上一次调用暂停的位置:let a = yield 1;
// 此时 'yield 1' 整个表达式的值被赋为我们传入的参数 'b'
// 因此本次的执行结果为:b {value: 2, done: false}// 第三次调用,传入一个参数 'c'
console.log(gen.next('c'))
// 与上面同理,上一次暂停位置的yield表达式 'yield 2' 的值被赋为 'c'
// 所以本次执行结果为:c {value: 3, done: false}// 最后一次调用,传入参数 'd'
console.log(gen.next('d'))
// 还是同理,'yield 3' 的值被赋为 'd',另外,生成器函数中有return,所以这里迭代器结果对象中的value有值了
// d {value: 100, done: false}
2.3 生成器的应用 — 模拟异步问题
function fa() {setTimeout(() => {let yon = '用户数据';//传入参数并调用han.next(yon);}, 1000);
}function fb() {setTimeout(() => {let din = '订单数据';han.next(din);}, 1000);
}function fc() {setTimeout(() => {let shang = '商品数据';han.next(shang);}, 1000);
}function* fn() {let a = yield fa();console.log(a);        //1s 用户数据let b = yield fb();console.log(b);        //2s 订单数据let c = yield fc();console.log(c);        //3s 商品数据
}
let han = fn();
//调用
han.next();

总的来说,生成器是ES6中提供的一种异步编程的解决方法,但通过上面的例子可以看到,单纯地利用生成器来实现异步操作,会导致代码可读性比较低,在实际开发中使用更多的还是 async/await 和 promise 。


http://www.ppmy.cn/news/1158603.html

相关文章

PLC 学习day01 了解PLC 的组成和知识。

1.资料来源 链接&#xff1a;3.三菱PLC编程视频关于PLC工作原理的介绍_哔哩哔哩_bilibili 2. PLC 的知识 2.1 PLC 的概述及特点功能 PLC是可编程逻辑控制器&#xff08;Programmable Logic Controller&#xff09;的英文缩写&#xff0c;是融合了继电器控制功能和计算机运算功…

利用Python turtle绘制中国结附源码

一、中国结 01 平安喜乐 1&#xff09;效果图 import turtle turtle.screensize(600,800) turtle.pensize(10) turtle.pencolor("red") turtle.seth(-45) turtle.fd(102) turtle.circle(-6,180) turtle.fd(102) turtle.circle(6,180) turtle.fd(102) turtle.circle(…

dependencyManagement和dependencies的区别

它们两个的层级关系是&#xff1a;dependencyManagement>dependencies dependencies&#xff1a; 在dependencies标签里声明的依赖都会自动引入&#xff0c;并默认被所有的子项目所继承(强制性的)。 dependencyManagement&#xff1a; 当一个项目包含多个模块时&#xff0…

leetcode 300. 最长递增子序列、674. 最长连续递增序列、718. 最长重复子数组

300. 最长递增子序列 给你一个整数数组 nums &#xff0c;找到其中最长严格递增子序列的长度。 子序列 是由数组派生而来的序列&#xff0c;删除&#xff08;或不删除&#xff09;数组中的元素而不改变其余元素的顺序。例如&#xff0c;[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子…

dubbo-admin安装

一、dubbo-admin安装 1、环境准备 dubbo-admin 是一个前后端分离的项目。前端使用vue&#xff0c;后端使用springboot&#xff0c;安装 dubbo-admin 其实就是部署该项目。我们将dubbo-admin安装到开发环境上。要保证开发环境有jdk&#xff0c;maven&#xff0c;nodejs 安装no…

众和策略:几点开盘和收盘股票?

股票开盘和收盘时间是投资者有必要知道的要害信息&#xff0c;因为它们挑选了股票生意的初步和结束时间。在此文章中&#xff0c;咱们将从多个视点分析股票开盘和收盘时间&#xff0c;包括全球商场开盘时间、技术分析对开盘前后价格不坚决的影响、以及日内生意者如安在开盘和收…

android U广播详解(二)

android U广播详解&#xff08;一&#xff09; 基础代码介绍 广播相关 // 用作单个进程批量分发receivers&#xff0c;已被丢弃 frameworks/base/services/core/java/com/android/server/am/BroadcastReceiverBatch.java // 主要逻辑所在类&#xff0c;包括入队、分发、结束…

Constitutional AI

用中文以结构树的方式列出这篇讲稿的知识点&#xff1a; Although you can use a reward model to eliminate the need for human evaluation during RLHF fine tuning, the human effort required to produce the trained reward model in the first place is huge. The label…