迭代器
什么是迭代器?
迭代器(iterator),是确使用户可在容器对象(container,例如链表或数组)上遍访的对象,使用该接口无需关心对象的内部实现细节。它提供了一种获取一系列值(无论是有限还是无限个)的标准方式。
其行为像数据库中的光标,迭代器最早出现在1974年设计的CLU编程语言中;
在各种编程语言的实现中,迭代器的实现方式各不相同,但是基本都有迭代器,比如Java、Python等;
说人话就是:迭代器是一个工具对象,可以帮助我们对某个数据结构进行遍历。
JavaScript 中的迭代器
在JavaScript中,迭代器也是一个具体的对象,这个对象需要符合迭代器协议规范(iterator protocol):
迭代器要求有一个获取值的标准方式,在 js 中这个标准方式就是迭代器对象中一个特定的 next 方法。
next 方法有如下的要求:
- 是一个无参数或者一个参数的函数,返回一个应当拥有以下两个属性的对象:
- done(boolean)
如果迭代器可以产生序列中的下一个值,则为 false。(这等价于没有指定 done 这个属性。)
如果迭代器已将序列迭代完毕,则为 true。这种情况下,value 是可选的,如果它依然存在,即为迭代结束之后的默认返回值。
- value
迭代器返回的任何 JavaScript 值。done 为 true 时可省略。
说人话就是:next 函数一个一个遍历容器,并返回一个对象。对象中有两个属性,done:表示该容器中是否还有值,如果没有值了为 true,表示遍历可以结束了;value:表示当前遍历出来的值,如果容器中没有值了,value 就是 undefined,此时 done 就是 true。
以一个数组为例:
// 待遍历的数组const arr = ['nba', 'cba', 'mba']// 数组迭代器迭代上面的数组let index = 0const iteratorArr = {next: function () {if (index < arr.length) {return { done: false, value: arr[index++] }} else {return { done: true, value: undefined }}}}console.log(iteratorArr.next());console.log(iteratorArr.next());console.log(iteratorArr.next());console.log(iteratorArr.next());console.log(iteratorArr.next());// { done: false, value: 'nba' }// { done: false, value: 'cba' }// { done: false, value: 'mba' }// { done: true, value: undefined }// { done: true, value: undefined }
什么是可迭代对象?
它和迭代器是不同的概念;
当一个对象实现了iterable protocol 可迭代协议时,它就是一个可迭代对象;
这个对象要求必须实现迭代器方法,并且要求使用 Symbol.iterator 作为迭代器方法的 key;
什么是可迭代协议?
可迭代协议就像一个接口,要想能被迭代,就得实现这个接口,也就是得按接口的规矩办事。很多容器就实现了这个接口,所以我们可以遍历出容器中的元素。换句话说,很多容器对象都是可迭代对象,并且是否实现可迭代协议,也是一个容器能否被遍历取值的必要条件。(难怪 Java 中这些集合都实现了 iterator 接口)
一个可迭代容器示例:
// 一个写死的容器(可迭代对象)const iterableObj = {// 假设容器中已经存了这些数据arr: ["abc", "cba", "nba"],// 迭代器方法也是写死的,只能遍历上面的测试数据[Symbol.iterator]: function() {let index = 0return {next: () => { // 小细节:迭代器对象中要想访问到测试数据数组,得用箭头函数,if (index < this.arr.length) { // 因为 this 可以跳过迭代器对象作用域指向容器return { done: false, value: this.arr[index++] }} else {return { done: true, value: undefined }}}}}}// 获取该容器的迭代器const iteratorArr = iterableObj[Symbol.iterator]()console.log(iteratorArr.next());console.log(iteratorArr.next());console.log(iteratorArr.next());console.log(iteratorArr.next());// { done: false, value: 'abc' }// { done: false, value: 'cba' }// { done: false, value: 'nba' }// { done: true, value: undefined }
当一个容器实现可迭代协议成为一个可迭代容器后,我们就可以利用这个容器可被迭代的能力,添加一些语法糖,来帮助使用容器的人更好的遍历容器取值。
比如 for … of 就是一个方便我们对可迭代对象进行迭代的语法糖。
// 一个写死的容器(可迭代对象)const iterableObj = {// 假设容器中已经存了这些数据arr: ["abc", "cba", "nba"],// 迭代器方法也是写死的,只能遍历上面的测试数据[Symbol.iterator]: function() {let index = 0return {next: () => { // 小细节:迭代器对象中要想访问到测试数据数组,得用箭头函数,if (index < this.arr.length) { // 因为 this 可以跳过迭代器对象作用域指向容器return { done: false, value: this.arr[index++] }} else {return { done: true, value: undefined }}}}}}for (const item of iterableObj) {console.log(item);}// 很方便的取出了值,而不用我们去 value 里面扣,这个糖真不错// abc// cba// nba
原生迭代器对象
JavaScript 中原生的容器都实现了可迭代协议,所以它们都是可迭代的,可迭代对象就是这些容器的实例。
- String、Array、Map、Set、arguments对象、NodeList集合 ```javascript // String for (const iterator of “hello”) { console.log(iterator); // h e l l o }
// array const arr = [‘abc’, ‘cba’, ‘nba’] for (const iterator of arr) { console.log(iterator); // abc cba nba }
// Map const map = new Map() map.set(‘key1’, ‘a’) map.set(‘key2’, ‘b’) map.set(‘key3’, ‘c’) for (const iterator of map) { console.log(iterator) // [ ‘key1’, ‘a’ ] [ ‘key2’, ‘b’ ] [ ‘key3’, ‘c’ ] }
// Set const set = new Set() set.add(‘a’) set.add(‘b’) set.add(‘c’) for (const iterator of set) { console.log(iterator); // a b c }
// 函数中arguments也是一个可迭代对象 function foo(x, y, z) { for (const arg of arguments) { console.log(arg) } } foo(10, 20, 30) // 10 20 30
<a name="Qp6eW"></a>## 可迭代对象的应用体现可迭代对象的应用体现在很多方面:- JavaScript中语法:for ...of、展开语法(spread syntax)、yield*(后面讲)、解构赋值(Destructuring_assignment);- 创建一些对象时:new Map([Iterable])、new WeakMap([iterable])、new Set([iterable])、new WeakSet([iterable]);- 一些方法的调用:Promise.all(iterable)、Promise.race(iterable)、Array.from(iterable);```javascript// 1.for of场景// 2.展开语法(spread syntax)const iterableObj = {names: ["abc", "cba", "nba"],[Symbol.iterator]: function() {let index = 0return {next: () => {if (index < this.names.length) {return { done: false, value: this.names[index++] }} else {return { done: true, value: undefined }}}}}}const arr = [1, 2, 3]const newNames = [...arr, ...iterableObj]console.log(newNames) // [ 1, 2, 3, 'abc', 'cba', 'nba' ]// 普通对象为什么也能用展开语法?const obj = { name: 'zs', age: 20}const objarr = {...obj}console.log(objarr); // { name: 'zs', age: 20 }// 因为这是 ES9(ES2018) 作了特殊处理,新增的一个特性: 内部实现用的不是迭代器。// ES9 之前是不能用展开语法的// 3.解构语法const [ name1, name2 ] = arr// 普通对象也能解构,也是 ES9 特意的处理,让解构支持普通对象,内部实现也不是迭代器const { name, age } = objconsole.log(name, age); // zs 20// 4.创建一些其他对象时,它会要求你传入一个可迭代对象,要不然报错// 这时我们就能知道什么能传什么不能传,更方便的阅读 APIconst set1 = new Set(iterableObj) // 自定义的容器实例const set2 = new Set(arr) // 数组实例const set3 = new Set('asdf') // String 实例// 数字就不是可迭代对象,它的构造函数中没有迭代器// const set4 = new Set(123) // number 123 is not iterable (cannot read property Symbol(Symbol.iterator))// 5.Promise.all// 开始我们只是传 promise 的数组,其实只要可迭代对象都行Promise.all(iterableObj).then(res => {console.log(res)})// 相当于:Promise.all( Promise.resolve(iterableObj) ).then()
手动实现一个简易可迭代容器
上面已经实现了一个写死的可迭代容器,我们可以稍微完善一下。
- 要想可迭代,这个容器就得实现可迭代协议。
- 容器中数据的存储结构,就用一个数组,并且存储结构挂载在容器属性上。
一些容器基本的方法,如添加方法,精确查找方法,删除方法就也免了。实例化的时候就添加数据。 ```javascript // 创建一个教室容器类, 创建出来的对象都是可迭代教室对象 class Classroom { constructor(address, name, students) { this.address = address this.name = name this.students = students }
Symbol.iterator { let index = 0 return { next: () => {
if (index < this.students.length) {return { done: false, value: this.students[index++] }} else {return { done: true, value: undefined }}
} } } }
const classroom = new Classroom(“3幢5楼205”, “计算机教室”, [“james”, “kobe”, “curry”])
// 迭代成功 for (const item of classroom) { console.log(item); // james kobe curry }
<a name="eclxI"></a>## 迭代器的终止迭代器一般情况下会迭代完所有元素,但也可再某些条件下中断:- 比如遍历的过程中通过 break、continue、return、throw 中断了循环操作;- 比如在解构的时候,没有解构所有的值;那么这个时候我们想要监听中断的话,可以在迭代器中添加 return 方法:```javascript// 创建一个教室容器类, 创建出来的对象都是可迭代教室对象class Classroom {constructor(address, name, students) {this.address = addressthis.name = namethis.students = students}[Symbol.iterator]() {let index = 0return { // 迭代器next: () => {if (index < this.students.length) {return { done: false, value: this.students[index++] }} else {return { done: true, value: undefined }}},// 迭代中断后的处理return: () => {console.log("迭代器提前终止了~")// 也需要返回一个对象,包含这两个属性,done 为 truereturn { done: true, value: undefined }}}}}const classroom = new Classroom("3幢5楼205", "计算机教室", ["james", "kobe", "curry" ])for (const item of classroom) {console.log(item); // james kobeif(item == 'kobe') break; // 迭代器提前终止了~}
生成器
什么是生成器?
生成器是ES6中新增的一种函数控制、使用的方案,它可以让我们更加灵活的控制函数什么时候继续执行、暂停执
行等。
平时我们会编写很多的函数,这些函数终止的条件通常是返回值或者发生了异常。
生成器函数也是一个函数,但是和普通的函数有一些区别:
- 首先,生成器函数需要在function的后面加一个符号:
* - 其次,生成器函数可以通过
yield关键字来控制函数的执行流程: 最后,生成器函数的返回值是一个 Generator(生成器)
生成器函数执行 yield
直接调用生成器函数是不会执行的,它只是返回了一个生成器对象,需要通过生成器对象的 next 方法才能执行函数中的代码。
执行到 yield 会中断函数执行,再次调用 next 方法就会继续往下执行。生成器事实上是一种特殊的迭代器,yield 好像把函数切成了几段。 ```javascript function* foo() { console.log(“函数开始执行~”)const value1 = 100 console.log(“第一段代码:”, value1) // 中断函数执行 yield
const value2 = 200 console.log(“第二段代码:”, value2) yield
const value3 = 300 console.log(“第三段代码:”, value3) yield
console.log(“函数执行结束~”) }
// 直接调用生成器函数是不会执行的 foo() // 调用生成器函数时, 会给我们返回一个生成器对象 const generator = foo() // 需要通过生成器对象的 next 方法执行,执行到 yield 会中断函数执行 generator.next() // 需要再次调用 next 方法 console.log(“——————-“) generator.next()
// 函数开始执行~
// 第一段代码: 100
// ——————-
// 第二段代码: 200
// generator.next()
// console.log(“—————“)
// generator.next()
生成器作为特殊的迭代器,next 函数也会返回一个拥有 done 和 value 属性的对象。**事实上生成器函数中 的 yield 就是一个特殊的 return 。**<br />next 函数中 return 会返回 done 和 value 属性的对象(没写 return 语句,就是默认 return undefined),其中 return 后面的值,就是 value 的值;done 的值为 true,表示函数迭代完了,也就是终止函数。结合前面终止迭代器的 return 函数,生成器中也有,它就相当于在 next 中添加了一个 return```javascriptfunction* foo1() {return ;}console.log(foo().next()) // { value: undefined, done: true }function* foo2() {return 123;}console.log(foo().next()) // { value: 123, done: true }function* foo3() {}console.log(foo().return(123)) // { value: 123, done: true }
而 yield 也会返回拥有 done 和 value 属性的对象,yield 后面的值,也会成为 value 的值。最大的区别是 done,yield 的 done 为 false,表示没迭代完,所以 yield 是中断函数执行,还可以恢复。
function* foo() {console.log("函数开始执行~")const value1 = 100console.log("第一段代码:", value1)// 中断函数执行yieldconst value2 = 200console.log("第二段代码:", value2)yield 123console.log("函数执行结束~")}const generator = foo()console.log(generator.next())// 函数开始执行~// 第一段代码: 100// { value: undefined, done: false }console.log(generator.next())// 第二段代码: 200// { value: 123, done: false }
生成器传递参数 - next 函数
函数既然可以暂停来分段执行,那么函数应该是可以传递参数的,我们是否可以给每个分段来传递参数呢?
答案是可以的;我们在调用 next 函数的时候,可以给它传递参数, 并且这个值会作为上一个 yield 语句的返回值,这样 next 本次要执行的代码块就能拿到这个传进来的值了。
也就是说 next 函数能为本次执行的函数代码块传参。
function* foo(num) {console.log("函数开始执行~")const value1 = 100console.log("第一段代码:", value1)// 中断函数执行const n = yield // next 的参数作为上一个 yield 的返回值const value2 = 200 + num + n // num 是整个函数的参数,n 是第二个 next 函数传进来的参数console.log("第二段代码:", value2)console.log("函数执行结束~")}const generator = foo(1)generator.next()generator.next(10)// 函数开始执行~// 第一段代码: 100// 第二段代码: 211// 函数执行结束~
生成器抛出异常 – throw函数
除了给生成器函数内部传递参数之外,也可以给生成器函数内部抛出异常,终止函数运行。它针对的是 yield 语句,所以捕获它的异常就能继续运行下去。catch 中也继续添加 yield 中断。
function* foo(num) {console.log("函数开始执行~")const value1 = 100console.log("第一段代码:", value1)// 发生异常终止函数运行const n = yield// 捕获异常// const n = null// try {// n = yield// } catch (error) {// console.log('异常捕获');// }const value2 = 200 + num + nconsole.log("第二段代码:", value2)console.log("函数执行结束~")}const generator = foo(1)generator.next()generator.throw('抛出异常')generator.next(10)// 函数开始执行~// 第一段代码: 100// const n = yield// ^// 抛出异常
生成器替代迭代器
我们发现生成器是一种特殊的迭代器,那么在某些情况下我们可以使用生成器来替代迭代器:
迭代器的 next 方法会返回一个对象{done: , value: },yield也会返回这样一个对象。
yield 就是迭代器的语法糖一样,它可以在生成器函数中像迭代器一样以上面对象的形式将值返回。
- 注意:yield 需要在生成器函数中使用,函数需要加
*```javascript function* generatorFn() { for (let i = 0; i < 3; i++) { yield i } } const generator = generatorFn() console.log(generator.next()); console.log(generator.next()); console.log(generator.next());
// { value: 0, done: false } // { value: 1, done: false } // { value: 2, done: false }
```javascript// 一个写死的容器(可迭代对象)const iterableObj = {arr: ["abc", "cba", "nba"],// 手动编写的迭代器函数,可以返回一个迭代器对象[Symbol.iterator]: function() {let index = 0return {next: () => {if (index < this.arr.length) {return { done: false, value: this.arr[index++] }} else {return { done: true, value: undefined }}}}}}console.log(iterableObj[Symbol.iterator]().next()); // { done: false, value: 'abc' }// 可以利用 yield 代替手动实现迭代器// yield 像 return 一样返回值,它取出可迭代对象中的值,并按迭代器返回对象的格式返回const iterableObj1 = {arr: ["abc", "cba", "nba"],// 改成生成器函数,并用 yield 代替 return 和 迭代器对象[Symbol.iterator]: function*() {let index = 0yield this.arr[index++]}}console.log(iterableObj1[Symbol.iterator]().next()); // { value: 'abc', done: false }
yield*
yield* 相当于 yield 的语法糖,语法糖的语法糖了属于是。yield* 可以直接迭代可迭代对象,手动取值都不需要了。
const iterableObj2 = {arr: ["abc", "cba", "nba"],// 改成生成器函数,并用 yield* 直接迭代可迭代对象[Symbol.iterator]: function*() {yield* this.arr}}console.log(iterableObj2[Symbol.iterator]().next()); // { value: 'abc', done: false }
自定义容器中也可以使用 yield* 来简化代码
// 创建一个教室容器类, 创建出来的对象都是可迭代教室对象class Classroom {constructor(address, name, students) {this.address = addressthis.name = namethis.students = students}// 注意:将函数改成生成器函数如果使用了键值对的语法糖, * 号加在最前面*[Symbol.iterator]() {yield* this.students}// 或者写成函数表达式的形式再加 * 号// [Symbol.iterator] = function* () {// yield* this.students// }}const classroom = new Classroom("3幢5楼205", "计算机教室", ["james", "kobe", "curry"])console.log(classroom[Symbol.iterator]().next()); // { value: 'james', done: false }
异步处理方案
学完了我们前面的Promise、生成器等,我们目前来看一下异步代码的最终处理方案。
需求:
- 我们需要向服务器发送网络请求获取数据,一共需要发送三次请求;
- 第二次的请求url依赖于第一次的结果;
- 第三次的请求url依赖于第二次的结果;
- 依次类推;
也就是说,我们要请求中发送请求,并且下一次的请求依赖上一次请求的结果。
最原始的方案:在请求成功后的 then 函数中套娃再次调用请求函数发送请求。但发生了回调地狱
// request.jsfunction requestData(url) {// 异步请求的代码会被放入到executor中return new Promise((resolve, reject) => {// 模拟网络请求setTimeout(() => {// 拿到请求的结果,这里 url 就是请求结果resolve(url)}, 2000)})}// 需求:// 1> url: why -> res: why// 2> url: res + "aaa" -> res: whyaaa// 3> url: res + "bbb" => res: whyaaabbb// 1.第一种方案: 多次回调// 回调地狱requestData('why').then(res => {requestData(res + 'aaa').then(res => {requestData(res + 'bbb').then(res => {console.log(res)})})})
第二种方案:利用 Promise 中 then 的返回值来解决。
之前第一次请求成功后,在 then 中再次调用 requestData 发送请求,然后再直接 .then。这样就造成回调嵌套。
requestData('why').then(res => {requestData(res + 'aaa').then() // 在嵌套中调用 then}
现在我们不直接调用 then 方法了,而是将 requestData 方法调用后 return 出去。requestData 函数返回一个 promise 对象,所以可以将下一个 then 方法,进行链式调用到当前 then 方法的后面。因为这样刚好能取到返回的 promise 对象的请求结果。
// 2.第二种方案: Promise中then的返回值来解决requestData("why").then(res => {return requestData(res + "aaa") // 这次请求的结果会作为 then 返回的 promise 的结果}).then(res => { // 这个 then 方法可以获取到回调中返回的 promise 的请求结果return requestData(res + "bbb")}).then(res => {console.log(res)})
但是这样阅读性还是不够清晰。
所以推出了第三种方案:promise + generator
// 先封装一个生成器函数function* getData() {yield requestData('why') // yield 会返回 value 为 promise 的对象}// 实现生成器函数const generator = getData()generator.next() // 拿到两个属性的对象.value // 从对象中取出了 promise 对象.then(res => {// 这里就拿到了当然 requestData 方法中 promise 的请求结果// 但是我们这里拿到了没有用啊,我们想要拿到结果能给下一次的 promise 请求})
这个时候我们就要想到 next 函数传递参数了,next 函数的参数会作为上一个 yield 表达式的返回值。
// 先封装一个生成器函数function* getData() {// res1 接收到了 next 的传参,也就是 res1 拿到了 requestData('why') 的请求结果const res1 = yield requestData('why')yield requestData(res1 + 'bbb') // 第二次的 requestData 就接收到了上一次请求的结果}// 执行生成器函数const generator = getData()generator.next().value.then(res => {// 这里就拿到了当前 requestData 方法中 promise 的请求结果 res// 但是我们这里拿到了没有用啊,我们想要拿到结果能给下一次的 promise 请求generator.next(res) // 这样一传参,就把 res 传给了上一个 yield 的返回值})
function* getData() {const res1 = yield requestData('why') // 多个请求就这样分块执行下去const res2 = yield requestData(res1 + 'bbb')yield requestData(res2 + 'ccc')}// 执行生成器函数const generator = getData()generator.next() .value .then(res => {generator.next(res).value.then(res => {generator.next(res).value.then(res => {console.log(res) // whybbbccc})})})
现在发送请求部分是分块了,很清晰,但是执行生成器函数的时候,又回调地狱了。但是这个回调是很有规律的,可以利用递归解决。
// 封装一个工具函数execGenerator自动执行生成器函数function execGenerator(genFn) {// 获取生成器const generator = genFn()// 递归函数function exec(res) {// 手动执行第一个步const result = generator.next(res)// 停止条件if (result.done) return result.value// 停止条件不成立,开始递归执行result.value.then(res => {exec(res)})}exec()}
除了手动封装生成器函数执行函数,还可以使用第三方库:co
// npm i coconst co = require('co')co(getData) // 将生产器函数传进去就可以了
终极方案:async/await
